diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..891f13c8a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,51 @@ +name: Build +on: [pull_request] +jobs: + build-mac: + name: Mac UI on ${{ matrix.os }} + strategy: + matrix: + os: [macos-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Make + working-directory: OSBindings/Mac + run: xcodebuild CODE_SIGN_IDENTITY=- + build-sdl: + name: SDL UI on ${{ matrix.os }} + strategy: + matrix: + os: [macos-latest, ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Install dependencies + shell: bash + run: | + case $RUNNER_OS in + Linux) + sudo apt-get --allow-releaseinfo-change update + sudo apt-get --fix-missing install gcc-10 libsdl2-dev scons + ;; + macOS) + brew install scons sdl2 + ;; + esac + - name: Make + working-directory: OSBindings/SDL + shell: bash + run: | + case $RUNNER_OS in + Linux) + jobs=$(nproc --all) + ;; + macOS) + jobs=$(sysctl -n hw.activecpu) + ;; + *) + jobs=1 + esac + scons -j"$jobs" diff --git a/.github/workflows/ccpp.yml b/.github/workflows/ccpp.yml deleted file mode 100644 index 27e180a99..000000000 --- a/.github/workflows/ccpp.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: SDL/Ubuntu - -on: [push, pull_request] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Install dependencies - run: sudo apt-get --allow-releaseinfo-change update && sudo apt-get --fix-missing install libsdl2-dev scons - - name: Make - working-directory: OSBindings/SDL - run: scons -j$(nproc --all) diff --git a/Activity/Observer.hpp b/Activity/Observer.hpp index 307b404be..9ed8f4583 100644 --- a/Activity/Observer.hpp +++ b/Activity/Observer.hpp @@ -6,9 +6,9 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef ActivityObserver_h -#define ActivityObserver_h +#pragma once +#include #include namespace Activity { @@ -23,6 +23,8 @@ namespace Activity { */ class Observer { public: + virtual ~Observer() {} + /// Provides hints as to the sort of information presented on an LED. enum LEDPresentation: uint8_t { /// This LED informs the user of some sort of persistent state, e.g. scroll lock. @@ -55,5 +57,3 @@ class Observer { }; } - -#endif /* ActivityObserver_h */ diff --git a/Activity/Source.hpp b/Activity/Source.hpp index 853c99210..6f540cf33 100644 --- a/Activity/Source.hpp +++ b/Activity/Source.hpp @@ -6,8 +6,7 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef ActivitySource_h -#define ActivitySource_h +#pragma once #include "Observer.hpp" @@ -19,6 +18,3 @@ class Source { }; } - - -#endif /* ActivitySource_h */ diff --git a/Analyser/Dynamic/ConfidenceCounter.hpp b/Analyser/Dynamic/ConfidenceCounter.hpp index 9e409592f..8b8e80ea2 100644 --- a/Analyser/Dynamic/ConfidenceCounter.hpp +++ b/Analyser/Dynamic/ConfidenceCounter.hpp @@ -6,13 +6,11 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef ConfidenceCounter_hpp -#define ConfidenceCounter_hpp +#pragma once #include "ConfidenceSource.hpp" -namespace Analyser { -namespace Dynamic { +namespace Analyser::Dynamic { /*! Provides a confidence source that calculates its probability by virtual of a history of events. @@ -42,6 +40,3 @@ class ConfidenceCounter: public ConfidenceSource { }; } -} - -#endif /* ConfidenceCounter_hpp */ diff --git a/Analyser/Dynamic/ConfidenceSource.hpp b/Analyser/Dynamic/ConfidenceSource.hpp index 0211a6d19..708627f66 100644 --- a/Analyser/Dynamic/ConfidenceSource.hpp +++ b/Analyser/Dynamic/ConfidenceSource.hpp @@ -6,11 +6,9 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef ConfidenceSource_hpp -#define ConfidenceSource_hpp +#pragma once -namespace Analyser { -namespace Dynamic { +namespace Analyser::Dynamic { /*! Provides an abstract interface through which objects can declare the probability @@ -23,6 +21,3 @@ struct ConfidenceSource { }; } -} - -#endif /* ConfidenceSource_hpp */ diff --git a/Analyser/Dynamic/ConfidenceSummary.hpp b/Analyser/Dynamic/ConfidenceSummary.hpp index ccab36761..880a9e8ea 100644 --- a/Analyser/Dynamic/ConfidenceSummary.hpp +++ b/Analyser/Dynamic/ConfidenceSummary.hpp @@ -6,15 +6,13 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef ConfidenceSummary_hpp -#define ConfidenceSummary_hpp +#pragma once #include "ConfidenceSource.hpp" #include -namespace Analyser { -namespace Dynamic { +namespace Analyser::Dynamic { /*! Summaries a collection of confidence sources by calculating their weighted sum. @@ -41,6 +39,3 @@ class ConfidenceSummary: public ConfidenceSource { }; } -} - -#endif /* ConfidenceSummary_hpp */ diff --git a/Analyser/Dynamic/MultiMachine/Implementation/MultiConfigurable.hpp b/Analyser/Dynamic/MultiMachine/Implementation/MultiConfigurable.hpp index 2595fc69c..c18e039f0 100644 --- a/Analyser/Dynamic/MultiMachine/Implementation/MultiConfigurable.hpp +++ b/Analyser/Dynamic/MultiMachine/Implementation/MultiConfigurable.hpp @@ -6,8 +6,7 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef MultiConfigurable_hpp -#define MultiConfigurable_hpp +#pragma once #include "../../../../Machines/DynamicMachine.hpp" #include "../../../../Configurable/Configurable.hpp" @@ -15,8 +14,7 @@ #include #include -namespace Analyser { -namespace Dynamic { +namespace Analyser::Dynamic { /*! Provides a class that multiplexes the configurable interface to multiple machines. @@ -37,6 +35,3 @@ class MultiConfigurable: public Configurable::Device { }; } -} - -#endif /* MultiConfigurable_hpp */ diff --git a/Analyser/Dynamic/MultiMachine/Implementation/MultiJoystickMachine.hpp b/Analyser/Dynamic/MultiMachine/Implementation/MultiJoystickMachine.hpp index 7c5457a16..85fe40091 100644 --- a/Analyser/Dynamic/MultiMachine/Implementation/MultiJoystickMachine.hpp +++ b/Analyser/Dynamic/MultiMachine/Implementation/MultiJoystickMachine.hpp @@ -6,16 +6,14 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef MultiJoystickMachine_hpp -#define MultiJoystickMachine_hpp +#pragma once #include "../../../../Machines/DynamicMachine.hpp" #include #include -namespace Analyser { -namespace Dynamic { +namespace Analyser::Dynamic { /*! Provides a class that multiplexes the joystick machine interface to multiple machines. @@ -35,6 +33,3 @@ class MultiJoystickMachine: public MachineTypes::JoystickMachine { }; } -} - -#endif /* MultiJoystickMachine_hpp */ diff --git a/Analyser/Dynamic/MultiMachine/Implementation/MultiKeyboardMachine.cpp b/Analyser/Dynamic/MultiMachine/Implementation/MultiKeyboardMachine.cpp index d2557968a..d5a42ba5b 100644 --- a/Analyser/Dynamic/MultiMachine/Implementation/MultiKeyboardMachine.cpp +++ b/Analyser/Dynamic/MultiMachine/Implementation/MultiKeyboardMachine.cpp @@ -56,10 +56,10 @@ MultiKeyboardMachine::MultiKeyboard::MultiKeyboard(const std::vector<::MachineTy } } -bool MultiKeyboardMachine::MultiKeyboard::set_key_pressed(Key key, char value, bool is_pressed) { +bool MultiKeyboardMachine::MultiKeyboard::set_key_pressed(Key key, char value, bool is_pressed, bool is_repeat) { bool was_consumed = false; for(const auto &machine: machines_) { - was_consumed |= machine->get_keyboard().set_key_pressed(key, value, is_pressed); + was_consumed |= machine->get_keyboard().set_key_pressed(key, value, is_pressed, is_repeat); } return was_consumed; } diff --git a/Analyser/Dynamic/MultiMachine/Implementation/MultiKeyboardMachine.hpp b/Analyser/Dynamic/MultiMachine/Implementation/MultiKeyboardMachine.hpp index 2de296456..2bda83513 100644 --- a/Analyser/Dynamic/MultiMachine/Implementation/MultiKeyboardMachine.hpp +++ b/Analyser/Dynamic/MultiMachine/Implementation/MultiKeyboardMachine.hpp @@ -6,8 +6,7 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef MultiKeyboardMachine_hpp -#define MultiKeyboardMachine_hpp +#pragma once #include "../../../../Machines/DynamicMachine.hpp" #include "../../../../Machines/KeyboardMachine.hpp" @@ -15,8 +14,7 @@ #include #include -namespace Analyser { -namespace Dynamic { +namespace Analyser::Dynamic { /*! Provides a class that multiplexes the keyboard machine interface to multiple machines. @@ -32,7 +30,7 @@ class MultiKeyboardMachine: public MachineTypes::KeyboardMachine { public: MultiKeyboard(const std::vector &machines); - bool set_key_pressed(Key key, char value, bool is_pressed) final; + bool set_key_pressed(Key key, char value, bool is_pressed, bool is_repeat) final; void reset_all_keys() final; const std::set &observed_keys() const final; bool is_exclusive() const final; @@ -56,6 +54,3 @@ class MultiKeyboardMachine: public MachineTypes::KeyboardMachine { }; } -} - -#endif /* MultiKeyboardMachine_hpp */ diff --git a/Analyser/Dynamic/MultiMachine/Implementation/MultiMediaTarget.hpp b/Analyser/Dynamic/MultiMachine/Implementation/MultiMediaTarget.hpp index 274261462..09a1f39d6 100644 --- a/Analyser/Dynamic/MultiMachine/Implementation/MultiMediaTarget.hpp +++ b/Analyser/Dynamic/MultiMachine/Implementation/MultiMediaTarget.hpp @@ -6,8 +6,7 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef MultiMediaTarget_hpp -#define MultiMediaTarget_hpp +#pragma once #include "../../../../Machines/MediaTarget.hpp" #include "../../../../Machines/DynamicMachine.hpp" @@ -15,8 +14,7 @@ #include #include -namespace Analyser { -namespace Dynamic { +namespace Analyser::Dynamic { /*! Provides a class that multiplexes the media target interface to multiple machines. @@ -36,6 +34,3 @@ struct MultiMediaTarget: public MachineTypes::MediaTarget { }; } -} - -#endif /* MultiMediaTarget_hpp */ diff --git a/Analyser/Dynamic/MultiMachine/Implementation/MultiProducer.hpp b/Analyser/Dynamic/MultiMachine/Implementation/MultiProducer.hpp index 02e27c4df..35e153a84 100644 --- a/Analyser/Dynamic/MultiMachine/Implementation/MultiProducer.hpp +++ b/Analyser/Dynamic/MultiMachine/Implementation/MultiProducer.hpp @@ -6,8 +6,7 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef MultiProducer_hpp -#define MultiProducer_hpp +#pragma once #include "../../../../Concurrency/AsyncTaskQueue.hpp" #include "../../../../Machines/MachineTypes.hpp" @@ -19,8 +18,7 @@ #include #include -namespace Analyser { -namespace Dynamic { +namespace Analyser::Dynamic { template class MultiInterface { public: @@ -116,7 +114,3 @@ class MultiAudioProducer: public MultiInterface, pu */ } -} - - -#endif /* MultiProducer_hpp */ diff --git a/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.hpp b/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.hpp index e5946da1a..0a1990157 100644 --- a/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.hpp +++ b/Analyser/Dynamic/MultiMachine/Implementation/MultiSpeaker.hpp @@ -6,8 +6,7 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef MultiSpeaker_hpp -#define MultiSpeaker_hpp +#pragma once #include "../../../../Machines/DynamicMachine.hpp" #include "../../../../Outputs/Speaker/Speaker.hpp" @@ -16,8 +15,7 @@ #include #include -namespace Analyser { -namespace Dynamic { +namespace Analyser::Dynamic { /*! Provides a class that multiplexes calls to and from Outputs::Speaker::Speaker in order @@ -56,6 +54,3 @@ class MultiSpeaker: public Outputs::Speaker::Speaker, Outputs::Speaker::Speaker: }; } -} - -#endif /* MultiSpeaker_hpp */ diff --git a/Analyser/Dynamic/MultiMachine/MultiMachine.cpp b/Analyser/Dynamic/MultiMachine/MultiMachine.cpp index 8f96498cc..2f20b4a30 100644 --- a/Analyser/Dynamic/MultiMachine/MultiMachine.cpp +++ b/Analyser/Dynamic/MultiMachine/MultiMachine.cpp @@ -11,6 +11,12 @@ #include +namespace { + +Log::Logger logger; + +} + using namespace Analyser::Dynamic; MultiMachine::MultiMachine(std::vector> &&machines) : @@ -61,13 +67,14 @@ bool MultiMachine::would_collapse(const std::vectortimed_machine(); - LOGNBR(PADHEX(2) << timed_machine->get_confidence() << " " << timed_machine->debug_type() << "; "); + + if constexpr (logger.enabled) { + auto line = logger.info(); + for(const auto &machine: machines_) { + auto timed_machine = machine->timed_machine(); + line.append("%0.4f %s; ", timed_machine->get_confidence(), timed_machine->debug_type().c_str()); + } } - LOGNBR(std::endl); -#endif DynamicMachine *front = machines_.front().get(); std::stable_sort(machines_.begin(), machines_.end(), diff --git a/Analyser/Dynamic/MultiMachine/MultiMachine.hpp b/Analyser/Dynamic/MultiMachine/MultiMachine.hpp index ec7e10f48..2b42b4f97 100644 --- a/Analyser/Dynamic/MultiMachine/MultiMachine.hpp +++ b/Analyser/Dynamic/MultiMachine/MultiMachine.hpp @@ -6,8 +6,7 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef MultiMachine_hpp -#define MultiMachine_hpp +#pragma once #include "../../../Machines/DynamicMachine.hpp" @@ -22,8 +21,7 @@ #include #include -namespace Analyser { -namespace Dynamic { +namespace Analyser::Dynamic { /*! Provides the same interface as to a single machine, while multiplexing all @@ -81,6 +79,3 @@ class MultiMachine: public ::Machine::DynamicMachine, public MultiTimedMachine:: }; } -} - -#endif /* MultiMachine_hpp */ diff --git a/Analyser/Machines.hpp b/Analyser/Machines.hpp index bf4d408e7..280994c17 100644 --- a/Analyser/Machines.hpp +++ b/Analyser/Machines.hpp @@ -6,8 +6,7 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef Machines_h -#define Machines_h +#pragma once namespace Analyser { @@ -25,11 +24,10 @@ enum class Machine { MasterSystem, MSX, Oric, + PCCompatible, Vic20, ZX8081, ZXSpectrum, }; } - -#endif /* Machines_h */ diff --git a/Analyser/Static/Acorn/Disk.cpp b/Analyser/Static/Acorn/Disk.cpp index 220f6d5c9..9b1516381 100644 --- a/Analyser/Static/Acorn/Disk.cpp +++ b/Analyser/Static/Acorn/Disk.cpp @@ -19,10 +19,10 @@ using namespace Analyser::Static::Acorn; std::unique_ptr Analyser::Static::Acorn::GetDFSCatalogue(const std::shared_ptr &disk) { // c.f. http://beebwiki.mdfs.net/Acorn_DFS_disc_format auto catalogue = std::make_unique(); - Storage::Encodings::MFM::Parser parser(false, disk); + Storage::Encodings::MFM::Parser parser(Storage::Encodings::MFM::Density::Single, disk); - const Storage::Encodings::MFM::Sector *const names = parser.get_sector(0, 0, 0); - const Storage::Encodings::MFM::Sector *const details = parser.get_sector(0, 0, 1); + const Storage::Encodings::MFM::Sector *const names = parser.sector(0, 0, 0); + const Storage::Encodings::MFM::Sector *const details = parser.sector(0, 0, 1); if(!names || !details) return nullptr; if(names->samples.empty() || details->samples.empty()) return nullptr; @@ -65,7 +65,7 @@ std::unique_ptr Analyser::Static::Acorn::GetDFSCatalogue(const std::s uint8_t track = uint8_t(start_sector / 10); start_sector++; - Storage::Encodings::MFM::Sector *next_sector = parser.get_sector(0, track, sector); + const Storage::Encodings::MFM::Sector *next_sector = parser.sector(0, track, sector); if(!next_sector) break; long length_from_sector = std::min(data_length, 256l); @@ -84,15 +84,15 @@ std::unique_ptr Analyser::Static::Acorn::GetDFSCatalogue(const std::s */ std::unique_ptr Analyser::Static::Acorn::GetADFSCatalogue(const std::shared_ptr &disk) { auto catalogue = std::make_unique(); - Storage::Encodings::MFM::Parser parser(true, disk); + Storage::Encodings::MFM::Parser parser(Storage::Encodings::MFM::Density::Double, disk); - Storage::Encodings::MFM::Sector *free_space_map_second_half = parser.get_sector(0, 0, 1); + const Storage::Encodings::MFM::Sector *free_space_map_second_half = parser.sector(0, 0, 1); if(!free_space_map_second_half) return nullptr; std::vector root_directory; root_directory.reserve(5 * 256); for(uint8_t c = 2; c < 7; c++) { - const Storage::Encodings::MFM::Sector *const sector = parser.get_sector(0, 0, c); + const Storage::Encodings::MFM::Sector *const sector = parser.sector(0, 0, c); if(!sector) return nullptr; root_directory.insert(root_directory.end(), sector->samples[0].begin(), sector->samples[0].end()); } @@ -166,7 +166,7 @@ std::unique_ptr Analyser::Static::Acorn::GetADFSCatalogue(const std:: new_file.data.reserve(size); while(new_file.data.size() < size) { - const Storage::Encodings::MFM::Sector *const sector = parser.get_sector(start_sector / (80 * 16), (start_sector / 16) % 80, start_sector % 16); + const Storage::Encodings::MFM::Sector *const sector = parser.sector(start_sector / (80 * 16), (start_sector / 16) % 80, start_sector % 16); if(!sector) break; const auto length_from_sector = std::min(size - new_file.data.size(), sector->samples[0].size()); diff --git a/Analyser/Static/Acorn/Disk.hpp b/Analyser/Static/Acorn/Disk.hpp index bd3b519fc..f77a0fe47 100644 --- a/Analyser/Static/Acorn/Disk.hpp +++ b/Analyser/Static/Acorn/Disk.hpp @@ -6,15 +6,12 @@ // Copyright 2016 Thomas Harte. All rights reserved. // -#ifndef StaticAnalyser_Acorn_Disk_hpp -#define StaticAnalyser_Acorn_Disk_hpp +#pragma once #include "File.hpp" #include "../../../Storage/Disk/Disk.hpp" -namespace Analyser { -namespace Static { -namespace Acorn { +namespace Analyser::Static::Acorn { /// Describes a DFS- or ADFS-format catalogue(/directory): the list of files available and the catalogue's boot option. struct Catalogue { @@ -32,7 +29,3 @@ std::unique_ptr GetDFSCatalogue(const std::shared_ptr GetADFSCatalogue(const std::shared_ptr &disk); } -} -} - -#endif /* Disk_hpp */ diff --git a/Analyser/Static/Acorn/File.hpp b/Analyser/Static/Acorn/File.hpp index f0a9d612e..00fc5a7b0 100644 --- a/Analyser/Static/Acorn/File.hpp +++ b/Analyser/Static/Acorn/File.hpp @@ -6,16 +6,13 @@ // Copyright 2016 Thomas Harte. All rights reserved. // -#ifndef StaticAnalyser_Acorn_File_hpp -#define StaticAnalyser_Acorn_File_hpp +#pragma once #include #include #include -namespace Analyser { -namespace Static { -namespace Acorn { +namespace Analyser::Static::Acorn { struct File { std::string name; @@ -61,7 +58,3 @@ struct File { }; } -} -} - -#endif /* File_hpp */ diff --git a/Analyser/Static/Acorn/StaticAnalyser.hpp b/Analyser/Static/Acorn/StaticAnalyser.hpp index d7acbbc38..35331b65f 100644 --- a/Analyser/Static/Acorn/StaticAnalyser.hpp +++ b/Analyser/Static/Acorn/StaticAnalyser.hpp @@ -6,21 +6,14 @@ // Copyright 2016 Thomas Harte. All rights reserved. // -#ifndef StaticAnalyser_Acorn_StaticAnalyser_hpp -#define StaticAnalyser_Acorn_StaticAnalyser_hpp +#pragma once #include "../StaticAnalyser.hpp" #include "../../../Storage/TargetPlatforms.hpp" #include -namespace Analyser { -namespace Static { -namespace Acorn { +namespace Analyser::Static::Acorn { TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); } -} -} - -#endif /* AcornAnalyser_hpp */ diff --git a/Analyser/Static/Acorn/Tape.cpp b/Analyser/Static/Acorn/Tape.cpp index c812d21ad..b162e2a56 100644 --- a/Analyser/Static/Acorn/Tape.cpp +++ b/Analyser/Static/Acorn/Tape.cpp @@ -20,7 +20,7 @@ static std::unique_ptr GetNextChunk(const std::shared_ptr> 1) | (parser.get_next_bit(tape) << 9) +#define shift() shift_register = (shift_register >> 1) | (parser.get_next_bit(tape) << 9) // find next area of high tone while(!tape->is_at_end() && (shift_register != 0x3ff)) { diff --git a/Analyser/Static/Acorn/Tape.hpp b/Analyser/Static/Acorn/Tape.hpp index a02f83ce5..d6978ba61 100644 --- a/Analyser/Static/Acorn/Tape.hpp +++ b/Analyser/Static/Acorn/Tape.hpp @@ -6,22 +6,15 @@ // Copyright 2016 Thomas Harte. All rights reserved. // -#ifndef StaticAnalyser_Acorn_Tape_hpp -#define StaticAnalyser_Acorn_Tape_hpp +#pragma once #include #include "File.hpp" #include "../../../Storage/Tape/Tape.hpp" -namespace Analyser { -namespace Static { -namespace Acorn { +namespace Analyser::Static::Acorn { std::vector GetFiles(const std::shared_ptr &tape); } -} -} - -#endif /* Tape_hpp */ diff --git a/Analyser/Static/Acorn/Target.hpp b/Analyser/Static/Acorn/Target.hpp index 17dad7393..47aea1ca9 100644 --- a/Analyser/Static/Acorn/Target.hpp +++ b/Analyser/Static/Acorn/Target.hpp @@ -6,16 +6,13 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_Acorn_Target_h -#define Analyser_Static_Acorn_Target_h +#pragma once #include "../../../Reflection/Struct.hpp" #include "../StaticAnalyser.hpp" #include -namespace Analyser { -namespace Static { -namespace Acorn { +namespace Analyser::Static::Acorn { struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl { bool has_acorn_adfs = false; @@ -38,7 +35,3 @@ struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl< }; } -} -} - -#endif /* Analyser_Static_Acorn_Target_h */ diff --git a/Analyser/Static/Amiga/StaticAnalyser.hpp b/Analyser/Static/Amiga/StaticAnalyser.hpp index 99b6169ec..4e6fa7501 100644 --- a/Analyser/Static/Amiga/StaticAnalyser.hpp +++ b/Analyser/Static/Amiga/StaticAnalyser.hpp @@ -6,22 +6,14 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_Amiga_StaticAnalyser_hpp -#define Analyser_Static_Amiga_StaticAnalyser_hpp +#pragma once #include "../StaticAnalyser.hpp" #include "../../../Storage/TargetPlatforms.hpp" #include -namespace Analyser { -namespace Static { -namespace Amiga { +namespace Analyser::Static::Amiga { TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); } -} -} - - -#endif /* Analyser_Static_Amiga_StaticAnalyser_hpp */ diff --git a/Analyser/Static/Amiga/Target.hpp b/Analyser/Static/Amiga/Target.hpp index a41d37dd9..f3bead884 100644 --- a/Analyser/Static/Amiga/Target.hpp +++ b/Analyser/Static/Amiga/Target.hpp @@ -6,15 +6,12 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_Amiga_Target_h -#define Analyser_Static_Amiga_Target_h +#pragma once #include "../../../Reflection/Struct.hpp" #include "../StaticAnalyser.hpp" -namespace Analyser { -namespace Static { -namespace Amiga { +namespace Analyser::Static::Amiga { struct Target: public Analyser::Static::Target, public Reflection::StructImpl { ReflectableEnum(ChipRAM, @@ -42,7 +39,3 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImpl &disk, const std::unique_ptr &target) { - Storage::Encodings::MFM::Parser parser(true, disk); - Storage::Encodings::MFM::Sector *boot_sector = parser.get_sector(0, 0, 0x41); + Storage::Encodings::MFM::Parser parser(Storage::Encodings::MFM::Density::Double, disk); + const Storage::Encodings::MFM::Sector *boot_sector = parser.sector(0, 0, 0x41); if(boot_sector != nullptr && !boot_sector->samples.empty() && boot_sector->samples[0].size() == 512) { // Check that the first 64 bytes of the sector aren't identical; if they are then probably // this disk was formatted and the filler byte never replaced. diff --git a/Analyser/Static/AmstradCPC/StaticAnalyser.hpp b/Analyser/Static/AmstradCPC/StaticAnalyser.hpp index 9195dac31..234a069c5 100644 --- a/Analyser/Static/AmstradCPC/StaticAnalyser.hpp +++ b/Analyser/Static/AmstradCPC/StaticAnalyser.hpp @@ -6,21 +6,14 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_AmstradCPC_StaticAnalyser_hpp -#define Analyser_Static_AmstradCPC_StaticAnalyser_hpp +#pragma once #include "../StaticAnalyser.hpp" #include "../../../Storage/TargetPlatforms.hpp" #include -namespace Analyser { -namespace Static { -namespace AmstradCPC { +namespace Analyser::Static::AmstradCPC { TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); } -} -} - -#endif /* Analyser_Static_AmstradCPC_StaticAnalyser_hpp */ diff --git a/Analyser/Static/AmstradCPC/Target.hpp b/Analyser/Static/AmstradCPC/Target.hpp index 6f708d4b0..9bffa3358 100644 --- a/Analyser/Static/AmstradCPC/Target.hpp +++ b/Analyser/Static/AmstradCPC/Target.hpp @@ -6,17 +6,14 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_AmstradCPC_Target_h -#define Analyser_Static_AmstradCPC_Target_h +#pragma once #include "../../../Reflection/Enum.hpp" #include "../../../Reflection/Struct.hpp" #include "../StaticAnalyser.hpp" #include -namespace Analyser { -namespace Static { -namespace AmstradCPC { +namespace Analyser::Static::AmstradCPC { struct Target: public Analyser::Static::Target, public Reflection::StructImpl { ReflectableEnum(Model, CPC464, CPC664, CPC6128); @@ -32,8 +29,3 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImpl -namespace Analyser { -namespace Static { -namespace AppleII { +namespace Analyser::Static::AppleII { TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); } -} -} - -#endif /* Analyser_Static_AppleII_StaticAnalyser_hpp */ diff --git a/Analyser/Static/AppleII/Target.hpp b/Analyser/Static/AppleII/Target.hpp index 030b81947..0ad262201 100644 --- a/Analyser/Static/AppleII/Target.hpp +++ b/Analyser/Static/AppleII/Target.hpp @@ -6,16 +6,13 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_AppleII_Target_h -#define Analyser_Static_AppleII_Target_h +#pragma once #include "../../../Reflection/Enum.hpp" #include "../../../Reflection/Struct.hpp" #include "../StaticAnalyser.hpp" -namespace Analyser { -namespace Static { -namespace AppleII { +namespace Analyser::Static::AppleII { struct Target: public Analyser::Static::Target, public Reflection::StructImpl { ReflectableEnum(Model, @@ -52,7 +49,3 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImpl -namespace Analyser { -namespace Static { -namespace AppleIIgs { +namespace Analyser::Static::AppleIIgs { TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); } -} -} - -#endif /* Analyser_Static_AppleIIgs_StaticAnalyser_hpp */ diff --git a/Analyser/Static/AppleIIgs/Target.hpp b/Analyser/Static/AppleIIgs/Target.hpp index a0eb988aa..99ba12028 100644 --- a/Analyser/Static/AppleIIgs/Target.hpp +++ b/Analyser/Static/AppleIIgs/Target.hpp @@ -6,16 +6,13 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_AppleIIgs_Target_h -#define Analyser_Static_AppleIIgs_Target_h +#pragma once #include "../../../Reflection/Enum.hpp" #include "../../../Reflection/Struct.hpp" #include "../StaticAnalyser.hpp" -namespace Analyser { -namespace Static { -namespace AppleIIgs { +namespace Analyser::Static::AppleIIgs { struct Target: public Analyser::Static::Target, public Reflection::StructImpl { ReflectableEnum(Model, @@ -43,7 +40,3 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImpl -namespace Analyser { -namespace Static { -namespace Atari2600 { +namespace Analyser::Static::Atari2600 { TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); } -} -} - -#endif /* StaticAnalyser_hpp */ diff --git a/Analyser/Static/Atari2600/Target.hpp b/Analyser/Static/Atari2600/Target.hpp index c372656ff..09bcd9843 100644 --- a/Analyser/Static/Atari2600/Target.hpp +++ b/Analyser/Static/Atari2600/Target.hpp @@ -6,14 +6,11 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_Atari2600_Target_h -#define Analyser_Static_Atari2600_Target_h +#pragma once #include "../StaticAnalyser.hpp" -namespace Analyser { -namespace Static { -namespace Atari2600 { +namespace Analyser::Static::Atari2600 { struct Target: public ::Analyser::Static::Target { enum class PagingModel { @@ -39,7 +36,3 @@ struct Target: public ::Analyser::Static::Target { }; } -} -} - -#endif /* Analyser_Static_Atari_Target_h */ diff --git a/Analyser/Static/AtariST/StaticAnalyser.hpp b/Analyser/Static/AtariST/StaticAnalyser.hpp index 54c041eed..a51bd3370 100644 --- a/Analyser/Static/AtariST/StaticAnalyser.hpp +++ b/Analyser/Static/AtariST/StaticAnalyser.hpp @@ -6,22 +6,14 @@ // Copyright © 2019 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_AtariST_StaticAnalyser_hpp -#define Analyser_Static_AtariST_StaticAnalyser_hpp +#pragma once #include "../StaticAnalyser.hpp" #include "../../../Storage/TargetPlatforms.hpp" #include -namespace Analyser { -namespace Static { -namespace AtariST { +namespace Analyser::Static::AtariST { TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); } -} -} - - -#endif /* Analyser_Static_AtariST_StaticAnalyser_hpp */ diff --git a/Analyser/Static/AtariST/Target.hpp b/Analyser/Static/AtariST/Target.hpp index b4d01279f..490342f87 100644 --- a/Analyser/Static/AtariST/Target.hpp +++ b/Analyser/Static/AtariST/Target.hpp @@ -6,15 +6,12 @@ // Copyright © 2019 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_AtariST_Target_h -#define Analyser_Static_AtariST_Target_h +#pragma once #include "../../../Reflection/Struct.hpp" #include "../StaticAnalyser.hpp" -namespace Analyser { -namespace Static { -namespace AtariST { +namespace Analyser::Static::AtariST { struct Target: public Analyser::Static::Target, public Reflection::StructImpl { ReflectableEnum(MemorySize, @@ -32,7 +29,3 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImpl -namespace Analyser { -namespace Static { -namespace Coleco { +namespace Analyser::Static::Coleco { TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); } -} -} - -#endif /* StaticAnalyser_hpp */ diff --git a/Analyser/Static/Commodore/Disk.cpp b/Analyser/Static/Commodore/Disk.cpp index d83d2a54c..e759a636f 100644 --- a/Analyser/Static/Commodore/Disk.cpp +++ b/Analyser/Static/Commodore/Disk.cpp @@ -37,7 +37,7 @@ class CommodoreGCRParser: public Storage::Disk::Controller { @returns a sector if one was found; @c nullptr otherwise. */ - std::shared_ptr get_sector(uint8_t track, uint8_t sector) { + std::shared_ptr sector(uint8_t track, uint8_t sector) { int difference = int(track) - int(track_); track_ = track; @@ -182,7 +182,7 @@ std::vector Analyser::Static::Commodore::GetFiles(const std::shared_ptrdata.begin(), sector->data.end()); next_track = sector->data[0]; @@ -221,7 +221,7 @@ std::vector Analyser::Static::Commodore::GetFiles(const std::shared_ptrdata[0]; diff --git a/Analyser/Static/Commodore/Disk.hpp b/Analyser/Static/Commodore/Disk.hpp index 486ea086d..a29da4266 100644 --- a/Analyser/Static/Commodore/Disk.hpp +++ b/Analyser/Static/Commodore/Disk.hpp @@ -6,22 +6,15 @@ // Copyright 2016 Thomas Harte. All rights reserved. // -#ifndef StaticAnalyser_Commodore_Disk_hpp -#define StaticAnalyser_Commodore_Disk_hpp +#pragma once #include "../../../Storage/Disk/Disk.hpp" #include "File.hpp" #include -namespace Analyser { -namespace Static { -namespace Commodore { +namespace Analyser::Static::Commodore { std::vector GetFiles(const std::shared_ptr &disk); } -} -} - -#endif /* Disk_hpp */ diff --git a/Analyser/Static/Commodore/File.hpp b/Analyser/Static/Commodore/File.hpp index 5f943a3c8..e84b00d50 100644 --- a/Analyser/Static/Commodore/File.hpp +++ b/Analyser/Static/Commodore/File.hpp @@ -6,15 +6,13 @@ // Copyright 2016 Thomas Harte. All rights reserved. // -#ifndef File_hpp -#define File_hpp +#pragma once +#include #include #include -namespace Analyser { -namespace Static { -namespace Commodore { +namespace Analyser::Static::Commodore { struct File { std::wstring name; @@ -36,7 +34,3 @@ struct File { }; } -} -} - -#endif /* File_hpp */ diff --git a/Analyser/Static/Commodore/StaticAnalyser.cpp b/Analyser/Static/Commodore/StaticAnalyser.cpp index 90bd0b624..5635b1d5b 100644 --- a/Analyser/Static/Commodore/StaticAnalyser.cpp +++ b/Analyser/Static/Commodore/StaticAnalyser.cpp @@ -93,7 +93,7 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media // make a first guess based on loading address switch(files.front().starting_address) { default: - LOG("Unrecognised loading address for Commodore program: " << PADHEX(4) << files.front().starting_address); + Log::Logger().error().append("Unrecognised loading address for Commodore program: %04x", files.front().starting_address); [[fallthrough]]; case 0x1001: memory_model = Target::MemoryModel::Unexpanded; @@ -188,8 +188,8 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media // Unhandled: // - // M6: this is a C64 file. - // MV: this is a Vic-20 file. + // M6: this is a C64 file. + // MV: this is a Vic-20 file. // J1/J2: this C64 file should have the primary joystick in slot 1/2. // RO: this disk image should be treated as read-only. } diff --git a/Analyser/Static/Commodore/StaticAnalyser.hpp b/Analyser/Static/Commodore/StaticAnalyser.hpp index 86d7c9bed..ed4091c83 100644 --- a/Analyser/Static/Commodore/StaticAnalyser.hpp +++ b/Analyser/Static/Commodore/StaticAnalyser.hpp @@ -6,21 +6,14 @@ // Copyright 2016 Thomas Harte. All rights reserved. // -#ifndef StaticAnalyser_Commodore_StaticAnalyser_hpp -#define StaticAnalyser_Commodore_StaticAnalyser_hpp +#pragma once #include "../StaticAnalyser.hpp" #include "../../../Storage/TargetPlatforms.hpp" #include -namespace Analyser { -namespace Static { -namespace Commodore { +namespace Analyser::Static::Commodore { TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); } -} -} - -#endif /* CommodoreAnalyser_hpp */ diff --git a/Analyser/Static/Commodore/Tape.hpp b/Analyser/Static/Commodore/Tape.hpp index 1dec3a1cc..278924bf6 100644 --- a/Analyser/Static/Commodore/Tape.hpp +++ b/Analyser/Static/Commodore/Tape.hpp @@ -6,20 +6,13 @@ // Copyright 2016 Thomas Harte. All rights reserved. // -#ifndef StaticAnalyser_Commodore_Tape_hpp -#define StaticAnalyser_Commodore_Tape_hpp +#pragma once #include "../../../Storage/Tape/Tape.hpp" #include "File.hpp" -namespace Analyser { -namespace Static { -namespace Commodore { +namespace Analyser::Static::Commodore { std::vector GetFiles(const std::shared_ptr &tape); } -} -} - -#endif /* Tape_hpp */ diff --git a/Analyser/Static/Commodore/Target.hpp b/Analyser/Static/Commodore/Target.hpp index 9da8506ec..78d4fdbfe 100644 --- a/Analyser/Static/Commodore/Target.hpp +++ b/Analyser/Static/Commodore/Target.hpp @@ -6,17 +6,14 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_Commodore_Target_h -#define Analyser_Static_Commodore_Target_h +#pragma once #include "../../../Reflection/Enum.hpp" #include "../../../Reflection/Struct.hpp" #include "../StaticAnalyser.hpp" #include -namespace Analyser { -namespace Static { -namespace Commodore { +namespace Analyser::Static::Commodore { struct Target: public Analyser::Static::Target, public Reflection::StructImpl { enum class MemoryModel { @@ -72,7 +69,3 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImpl; @@ -236,7 +236,7 @@ static void AddToDisassembly(PartialDisassembly &disassembly, const std::vector< case Instruction::Relative: { std::size_t operand_address = address_mapper(address); if(operand_address >= memory.size()) return; - address++; + ++address; instruction.operand = memory[operand_address]; } @@ -291,20 +291,34 @@ static void AddToDisassembly(PartialDisassembly &disassembly, const std::vector< } // Decide on overall flow control. - if(instruction.operation == Instruction::RTS || instruction.operation == Instruction::RTI) return; - if(instruction.operation == Instruction::BRK) return; // TODO: check whether IRQ vector is within memory range - if(instruction.operation == Instruction::JSR) { - disassembly.remaining_entry_points.push_back(instruction.operand); - } - if(instruction.operation == Instruction::JMP) { - if(instruction.addressing_mode == Instruction::Absolute) - disassembly.remaining_entry_points.push_back(instruction.operand); - return; - } + + // All relative instructions are flow control. if(instruction.addressing_mode == Instruction::Relative) { uint16_t destination = uint16_t(address + int8_t(instruction.operand)); disassembly.remaining_entry_points.push_back(destination); } + + switch(instruction.operation) { + default: break; + + case Instruction::KIL: + case Instruction::RTS: + case Instruction::RTI: + case Instruction::BRK: // TODO: check whether IRQ vector is within memory range. + disassembly.implicit_entry_points.push_back(address); + return; + + case Instruction::JMP: + // Adding a new entry point for relative jumps was handled above. + if(instruction.addressing_mode == Instruction::Absolute) { + disassembly.remaining_entry_points.push_back(instruction.operand); + } + return; + + case Instruction::JSR: + disassembly.remaining_entry_points.push_back(instruction.operand); + break; + } } } @@ -316,5 +330,5 @@ Disassembly Analyser::Static::MOS6502::Disassemble( const std::vector &memory, const std::function &address_mapper, std::vector entry_points) { - return Analyser::Static::Disassembly::Disassemble(memory, address_mapper, entry_points); + return Analyser::Static::Disassembly::Disassemble(memory, address_mapper, entry_points, false); } diff --git a/Analyser/Static/Disassembler/6502.hpp b/Analyser/Static/Disassembler/6502.hpp index 4c7e22748..c56ba0c0f 100644 --- a/Analyser/Static/Disassembler/6502.hpp +++ b/Analyser/Static/Disassembler/6502.hpp @@ -6,8 +6,7 @@ // Copyright 2016 Thomas Harte. All rights reserved. // -#ifndef StaticAnalyser_Disassembler_6502_hpp -#define StaticAnalyser_Disassembler_6502_hpp +#pragma once #include #include @@ -16,9 +15,7 @@ #include #include -namespace Analyser { -namespace Static { -namespace MOS6502 { +namespace Analyser::Static::MOS6502 { /*! Describes a 6502 instruciton: its address, the operation it performs, its addressing mode @@ -95,7 +92,3 @@ Disassembly Disassemble( std::vector entry_points); } -} -} - -#endif /* Disassembler6502_hpp */ diff --git a/Analyser/Static/Disassembler/AddressMapper.hpp b/Analyser/Static/Disassembler/AddressMapper.hpp index 0bb8988c0..c3ddb02b6 100644 --- a/Analyser/Static/Disassembler/AddressMapper.hpp +++ b/Analyser/Static/Disassembler/AddressMapper.hpp @@ -6,14 +6,11 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef AddressMapper_hpp -#define AddressMapper_hpp +#pragma once #include -namespace Analyser { -namespace Static { -namespace Disassembler { +namespace Analyser::Static::Disassembler { /*! Provides an address mapper that relocates a chunk of memory so that it starts at @@ -26,7 +23,3 @@ template std::function OffsetMapper(T start_address } } -} -} - -#endif /* AddressMapper_hpp */ diff --git a/Analyser/Static/Disassembler/Kernel.hpp b/Analyser/Static/Disassembler/Kernel.hpp index d18e9e915..c4166521a 100644 --- a/Analyser/Static/Disassembler/Kernel.hpp +++ b/Analyser/Static/Disassembler/Kernel.hpp @@ -6,47 +6,61 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef Kernel_hpp -#define Kernel_hpp +#pragma once -namespace Analyser { -namespace Static { -namespace Disassembly { +namespace Analyser::Static::Disassembly { template struct PartialDisassembly { D disassembly; std::vector remaining_entry_points; + std::vector implicit_entry_points; }; template D Disassemble( const std::vector &memory, const std::function &address_mapper, - std::vector entry_points) { + std::vector entry_points, + bool exhaustive) +{ PartialDisassembly partial_disassembly; partial_disassembly.remaining_entry_points = entry_points; while(!partial_disassembly.remaining_entry_points.empty()) { - // pull the next entry point from the back of the vector - S next_entry_point = partial_disassembly.remaining_entry_points.back(); - partial_disassembly.remaining_entry_points.pop_back(); + // Do a recursive-style disassembly for all current entry points. + while(!partial_disassembly.remaining_entry_points.empty()) { + // Pull the next entry point from the back of the vector. + const S next_entry_point = partial_disassembly.remaining_entry_points.back(); + partial_disassembly.remaining_entry_points.pop_back(); - // if that address has already been visited, forget about it - if( partial_disassembly.disassembly.instructions_by_address.find(next_entry_point) - != partial_disassembly.disassembly.instructions_by_address.end()) continue; + // If that address has already been visited, forget about it. + if( partial_disassembly.disassembly.instructions_by_address.find(next_entry_point) + != partial_disassembly.disassembly.instructions_by_address.end()) continue; - // if it's outgoing, log it as such and forget about it; otherwise disassemble - std::size_t mapped_entry_point = address_mapper(next_entry_point); - if(mapped_entry_point >= memory.size()) - partial_disassembly.disassembly.outward_calls.insert(next_entry_point); - else - Disassembler::AddToDisassembly(partial_disassembly, memory, address_mapper, next_entry_point); + // If it's outgoing, log it as such and forget about it; otherwise disassemble. + std::size_t mapped_entry_point = address_mapper(next_entry_point); + if(mapped_entry_point >= memory.size()) + partial_disassembly.disassembly.outward_calls.insert(next_entry_point); + else + Disassembler::AddToDisassembly(partial_disassembly, memory, address_mapper, next_entry_point); + } + + // If this is not an exhaustive disassembly, that's your lot. + if(!exhaustive) { + break; + } + + // Otherwise, copy in the new 'implicit entry points' (i.e. all locations that are one after + // a disassembled region). There's a test above that'll ignore any which have already been + // disassembled from. + std::move( + partial_disassembly.implicit_entry_points.begin(), + partial_disassembly.implicit_entry_points.end(), + std::back_inserter(partial_disassembly.remaining_entry_points) + ); + partial_disassembly.implicit_entry_points.clear(); } return partial_disassembly.disassembly; } } -} -} - -#endif /* Kernel_hpp */ diff --git a/Analyser/Static/Disassembler/Z80.cpp b/Analyser/Static/Disassembler/Z80.cpp index 96c113986..c35adbab1 100644 --- a/Analyser/Static/Disassembler/Z80.cpp +++ b/Analyser/Static/Disassembler/Z80.cpp @@ -11,7 +11,7 @@ #include "Kernel.hpp" using namespace Analyser::Static::Z80; -namespace { +namespace { using PartialDisassembly = Analyser::Static::Disassembly::PartialDisassembly; @@ -56,11 +56,11 @@ class Accessor { bool overrun_ = false; }; -#define x(v) (v >> 6) -#define y(v) ((v >> 3) & 7) -#define q(v) ((v >> 3) & 1) -#define p(v) ((v >> 4) & 3) -#define z(v) (v & 7) +constexpr uint8_t x(uint8_t v) { return v >> 6; } +constexpr uint8_t y(uint8_t v) { return (v >> 3) & 7; } +constexpr uint8_t q(uint8_t v) { return (v >> 3) & 1; } +constexpr uint8_t p(uint8_t v) { return (v >> 4) & 3; } +constexpr uint8_t z(uint8_t v) { return v & 7; } Instruction::Condition condition_table[] = { Instruction::Condition::NZ, Instruction::Condition::Z, @@ -589,7 +589,7 @@ struct Z80Disassembler { break; } - // Add any (potentially) newly discovered entry point. + // Add any (potentially) newly-discovered entry point. if( instruction.operation == Instruction::Operation::JP || instruction.operation == Instruction::Operation::JR || instruction.operation == Instruction::Operation::CALL || @@ -598,22 +598,37 @@ struct Z80Disassembler { } // This is it if: an unconditional RET, RETI, RETN, JP or JR is found. - if(instruction.condition != Instruction::Condition::None) continue; + switch(instruction.operation) { + default: break; - if(instruction.operation == Instruction::Operation::RET) return; - if(instruction.operation == Instruction::Operation::RETI) return; - if(instruction.operation == Instruction::Operation::RETN) return; - if(instruction.operation == Instruction::Operation::JP) return; - if(instruction.operation == Instruction::Operation::JR) return; + case Instruction::Operation::RET: + case Instruction::Operation::RETI: + case Instruction::Operation::RETN: + case Instruction::Operation::JP: + case Instruction::Operation::JR: + if(instruction.condition == Instruction::Condition::None) { + disassembly.implicit_entry_points.push_back(accessor.address()); + return; + } + } } } }; } // end of anonymous namespace + + Disassembly Analyser::Static::Z80::Disassemble( const std::vector &memory, const std::function &address_mapper, - std::vector entry_points) { - return Analyser::Static::Disassembly::Disassemble(memory, address_mapper, entry_points); + std::vector entry_points, + Approach approach) +{ + return Analyser::Static::Disassembly::Disassemble( + memory, + address_mapper, + entry_points, + approach == Approach::Exhaustive + ); } diff --git a/Analyser/Static/Disassembler/Z80.hpp b/Analyser/Static/Disassembler/Z80.hpp index 4f46b055d..69b1e6994 100644 --- a/Analyser/Static/Disassembler/Z80.hpp +++ b/Analyser/Static/Disassembler/Z80.hpp @@ -6,8 +6,7 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef StaticAnalyser_Disassembler_Z80_hpp -#define StaticAnalyser_Disassembler_Z80_hpp +#pragma once #include #include @@ -15,9 +14,7 @@ #include #include -namespace Analyser { -namespace Static { -namespace Z80 { +namespace Analyser::Static::Z80 { struct Instruction { /*! The address this instruction starts at. This is a mapped address. */ @@ -78,13 +75,18 @@ struct Disassembly { std::set internal_stores, internal_loads, internal_modifies; }; +enum class Approach { + /// Disassemble from the supplied entry points until an indeterminate branch or return only, adding other fully-static + /// entry points as they are observed. + Recursive, + /// Disassemble all supplied bytes, regardless of what nonsense may be encountered by accidental parsing of data areas. + Exhaustive, +}; + Disassembly Disassemble( const std::vector &memory, const std::function &address_mapper, - std::vector entry_points); + std::vector entry_points, + Approach approach); } -} -} - -#endif /* StaticAnalyser_Disassembler_Z80_hpp */ diff --git a/Analyser/Static/DiskII/StaticAnalyser.hpp b/Analyser/Static/DiskII/StaticAnalyser.hpp index a1f7a09f1..9f1da10d8 100644 --- a/Analyser/Static/DiskII/StaticAnalyser.hpp +++ b/Analyser/Static/DiskII/StaticAnalyser.hpp @@ -6,22 +6,14 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_DiskII_StaticAnalyser_hpp -#define Analyser_Static_DiskII_StaticAnalyser_hpp +#pragma once #include "../StaticAnalyser.hpp" #include "../../../Storage/TargetPlatforms.hpp" #include -namespace Analyser { -namespace Static { -namespace DiskII { +namespace Analyser::Static::DiskII { TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); } -} -} - - -#endif /* Analyser_Static_DiskII_StaticAnalyser_hpp */ diff --git a/Analyser/Static/Enterprise/StaticAnalyser.cpp b/Analyser/Static/Enterprise/StaticAnalyser.cpp index ec0904ab0..02a3166f0 100644 --- a/Analyser/Static/Enterprise/StaticAnalyser.cpp +++ b/Analyser/Static/Enterprise/StaticAnalyser.cpp @@ -40,6 +40,8 @@ Analyser::Static::TargetList Analyser::Static::Enterprise::GetTargets(const Medi target->basic_version = Target::BASICVersion::Any; // Inspect any supplied disks. + // + // TODO: how best can these be discerned from MS-DOS and MSX disks? if(!media.disks.empty()) { // DOS will be needed. target->dos = Target::DOS::EXDOS; diff --git a/Analyser/Static/Enterprise/StaticAnalyser.hpp b/Analyser/Static/Enterprise/StaticAnalyser.hpp index 0d7435d0a..c88aa43c3 100644 --- a/Analyser/Static/Enterprise/StaticAnalyser.hpp +++ b/Analyser/Static/Enterprise/StaticAnalyser.hpp @@ -6,22 +6,14 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_Enterprise_StaticAnalyser_hpp -#define Analyser_Static_Enterprise_StaticAnalyser_hpp +#pragma once #include "../StaticAnalyser.hpp" #include "../../../Storage/TargetPlatforms.hpp" #include -namespace Analyser { -namespace Static { -namespace Enterprise { +namespace Analyser::Static::Enterprise { TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); } -} -} - - -#endif /* Analyser_Static_Enterprise_StaticAnalyser_hpp */ diff --git a/Analyser/Static/Enterprise/Target.hpp b/Analyser/Static/Enterprise/Target.hpp index 957852c5f..b6238bf92 100644 --- a/Analyser/Static/Enterprise/Target.hpp +++ b/Analyser/Static/Enterprise/Target.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_Enterprise_Target_h -#define Analyser_Static_Enterprise_Target_h +#pragma once #include "../../../Reflection/Enum.hpp" #include "../../../Reflection/Struct.hpp" @@ -15,9 +14,7 @@ #include -namespace Analyser { -namespace Static { -namespace Enterprise { +namespace Analyser::Static::Enterprise { struct Target: public Analyser::Static::Target, public Reflection::StructImpl { ReflectableEnum(Model, Enterprise64, Enterprise128, Enterprise256); @@ -51,7 +48,3 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImplget_maximum_head_position() <= Storage::Disk::HeadPosition(40)) { + return Analyser::Static::PCCompatible::GetTargets(media, file_name, platforms); + } + + // Attempt to grab MFM track 0, sector 1: the boot sector. + const auto track_zero = disk->get_track_at_position(Storage::Disk::Track::Address(0, Storage::Disk::HeadPosition(0))); + const auto sector_map = Storage::Encodings::MFM::sectors_from_segment( + Storage::Disk::track_serialisation( + *track_zero, + Storage::Encodings::MFM::MFMBitLength + ), Storage::Encodings::MFM::Density::Double); + + // If no sectors were found, assume this disk was either single density or high density, which both imply the PC. + if(sector_map.empty() || sector_map.size() > 10) { + return Analyser::Static::PCCompatible::GetTargets(media, file_name, platforms); + } + + const Storage::Encodings::MFM::Sector *boot_sector = nullptr; + for(const auto &pair: sector_map) { + if(pair.second.address.sector == 1) { + boot_sector = &pair.second; + break; + } + } + + // This shouldn't technically be possible since the disk has been identified as FAT12, but be safe. + if(!boot_sector) { + return {}; + } + + // Check for key phrases that imply a PC disk. + const auto &sample = boot_sector->samples[0]; + const std::vector pc_strings = { + // MS-DOS strings. + "MSDOS", + "Non-System disk or disk error", + // DOS Plus strings. + "Insert a SYSTEM disk", + }; + for(const auto &string: pc_strings) { + if( + std::search(sample.begin(), sample.end(), string.begin(), string.end()) != sample.end() + ) { + return Analyser::Static::PCCompatible::GetTargets(media, file_name, platforms); + } + } + + // TODO: look for a COM, EXE or BAT, inspect. AUTOEXEC.BAT and/or CONFIG.SYS could be either PC or MSX. + // Disassembling the boot sector doesn't necessarily work, as several Enterprise titles out there in the wild seem + // to have been created by WINIMAGE which adds an x86 PC-style boot sector. + + // Enterprise notes: EXOS files all start with a 16-byte header which should begin with a 0 and then have a type + // byte that will be 0xa or lower; cf http://epbas.lgb.hu/readme.html + // + // Some disks commonly passed around as Enterprise software are actually CP/M software, expecting IS-DOS (the CP/M + // clone) to be present. It's certainly possible the same could be true of MSX disks and MSX-DOS. So analysing COM + // files probably means searching for CALL 5s and/or INT 21hs, if not a more rigorous disassembly. + // + // I have not been able to locate a copy of IS-DOS so there's probably not much that can be done here; perhaps I + // could redirect to an MSX2 with MSX-DOS2? Though it'd be nicer if I had a machine that was pure CP/M. + + // Being unable to prove that this is a PC disk, throw it to the Enterprise. + return Analyser::Static::Enterprise::GetTargets(media, file_name, platforms); +} diff --git a/Analyser/Static/FAT12/StaticAnalyser.hpp b/Analyser/Static/FAT12/StaticAnalyser.hpp new file mode 100644 index 000000000..52e8e538a --- /dev/null +++ b/Analyser/Static/FAT12/StaticAnalyser.hpp @@ -0,0 +1,19 @@ +// +// StaticAnalyser.hpp +// Clock Signal +// +// Created by Thomas Harte on 05/12/2023. +// Copyright 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "../StaticAnalyser.hpp" +#include "../../../Storage/TargetPlatforms.hpp" +#include + +namespace Analyser::Static::FAT12 { + +TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); + +} diff --git a/Analyser/Static/MSX/Cartridge.hpp b/Analyser/Static/MSX/Cartridge.hpp index 7e7a63a2e..a5436d483 100644 --- a/Analyser/Static/MSX/Cartridge.hpp +++ b/Analyser/Static/MSX/Cartridge.hpp @@ -6,14 +6,11 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef Cartridge_hpp -#define Cartridge_hpp +#pragma once #include "../../../Storage/Cartridge/Cartridge.hpp" -namespace Analyser { -namespace Static { -namespace MSX { +namespace Analyser::Static::MSX { /*! Extends the base cartridge class by adding a (guess at) the banking scheme. @@ -34,7 +31,3 @@ struct Cartridge: public ::Storage::Cartridge::Cartridge { }; } -} -} - -#endif /* Cartridge_hpp */ diff --git a/Analyser/Static/MSX/StaticAnalyser.cpp b/Analyser/Static/MSX/StaticAnalyser.cpp index b2bdf02de..2b8f6415d 100644 --- a/Analyser/Static/MSX/StaticAnalyser.cpp +++ b/Analyser/Static/MSX/StaticAnalyser.cpp @@ -37,6 +37,11 @@ static std::unique_ptr CartridgeTarget( auto target = std::make_unique(); target->confidence = confidence; + // Observation: all ROMs of 48kb or less are from the MSX 1 era. + if(segment.data.size() < 48*1024) { + target->model = Analyser::Static::MSX::Target::Model::MSX1; + } + if(type == Analyser::Static::MSX::Cartridge::Type::None) { target->media.cartridges.emplace_back(new Storage::Cartridge::Cartridge(output_segments)); } else { @@ -100,6 +105,7 @@ static Analyser::Static::TargetList CartridgeTargetsFrom( // TODO: check for a rational init address? // If this ROM is less than 48kb in size then it's an ordinary ROM. Just emplace it and move on. + // Bonus observation: all such ROMs are from the MSX 1 era. if(data_size <= 0xc000) { targets.emplace_back(CartridgeTarget(segment, start_address, Analyser::Static::MSX::Cartridge::Type::None, 1.0)); continue; @@ -109,97 +115,16 @@ static Analyser::Static::TargetList CartridgeTargetsFrom( // be at play; disassemble to try to figure it out. std::vector first_8k; first_8k.insert(first_8k.begin(), segment.data.begin(), segment.data.begin() + 8192); - Analyser::Static::Z80::Disassembly disassembly = + const Analyser::Static::Z80::Disassembly disassembly = Analyser::Static::Z80::Disassemble( first_8k, Analyser::Static::Disassembler::OffsetMapper(start_address), - { init_address } + { init_address }, + Analyser::Static::Z80::Approach::Exhaustive ); -// // Look for a indirect store followed by an unconditional JP or CALL into another -// // segment, that's a fairly explicit sign where found. using Instruction = Analyser::Static::Z80::Instruction; - std::map &instructions = disassembly.instructions_by_address; - bool is_ascii = false; -// auto iterator = instructions.begin(); -// while(iterator != instructions.end()) { -// auto next_iterator = iterator; -// next_iterator++; -// if(next_iterator == instructions.end()) break; -// -// if( iterator->second.operation == Instruction::Operation::LD && -// iterator->second.destination == Instruction::Location::Operand_Indirect && -// ( -// iterator->second.operand == 0x5000 || -// iterator->second.operand == 0x6000 || -// iterator->second.operand == 0x6800 || -// iterator->second.operand == 0x7000 || -// iterator->second.operand == 0x77ff || -// iterator->second.operand == 0x7800 || -// iterator->second.operand == 0x8000 || -// iterator->second.operand == 0x9000 || -// iterator->second.operand == 0xa000 -// ) && -// ( -// next_iterator->second.operation == Instruction::Operation::CALL || -// next_iterator->second.operation == Instruction::Operation::JP -// ) && -// ((next_iterator->second.operand >> 13) != (0x4000 >> 13)) -// ) { -// const uint16_t address = uint16_t(next_iterator->second.operand); -// switch(iterator->second.operand) { -// case 0x6000: -// if(address >= 0x6000 && address < 0x8000) { -// target.msx.cartridge_type = Analyser::Static::MSXCartridgeType::KonamiWithSCC; -// } -// break; -// case 0x6800: -// if(address >= 0x6000 && address < 0x6800) { -// target.msx.cartridge_type = Analyser::Static::MSXCartridgeType::ASCII8kb; -// } -// break; -// case 0x7000: -// if(address >= 0x6000 && address < 0x8000) { -// target.msx.cartridge_type = Analyser::Static::MSXCartridgeType::KonamiWithSCC; -// } -// if(address >= 0x7000 && address < 0x7800) { -// is_ascii = true; -// } -// break; -// case 0x77ff: -// if(address >= 0x7000 && address < 0x7800) { -// target.msx.cartridge_type = Analyser::Static::MSXCartridgeType::ASCII16kb; -// } -// break; -// case 0x7800: -// if(address >= 0xa000 && address < 0xc000) { -// target.msx.cartridge_type = Analyser::Static::MSXCartridgeType::ASCII8kb; -// } -// break; -// case 0x8000: -// if(address >= 0x8000 && address < 0xa000) { -// target.msx.cartridge_type = Analyser::Static::MSXCartridgeType::KonamiWithSCC; -// } -// break; -// case 0x9000: -// if(address >= 0x8000 && address < 0xa000) { -// target.msx.cartridge_type = Analyser::Static::MSXCartridgeType::KonamiWithSCC; -// } -// break; -// case 0xa000: -// if(address >= 0xa000 && address < 0xc000) { -// target.msx.cartridge_type = Analyser::Static::MSXCartridgeType::Konami; -// } -// break; -// case 0xb000: -// if(address >= 0xa000 && address < 0xc000) { -// target.msx.cartridge_type = Analyser::Static::MSXCartridgeType::KonamiWithSCC; -// } -// break; -// } -// } -// -// iterator = next_iterator; + const std::map &instructions = disassembly.instructions_by_address; // Look for LD (nnnn), A instructions, and collate those addresses. std::map address_counts; @@ -211,49 +136,46 @@ static Analyser::Static::TargetList CartridgeTargetsFrom( } } - // Weight confidences by number of observed hits. - float total_hits = - float( - address_counts[0x6000] + address_counts[0x6800] + - address_counts[0x7000] + address_counts[0x7800] + - address_counts[0x77ff] + address_counts[0x8000] + - address_counts[0xa000] + address_counts[0x5000] + - address_counts[0x9000] + address_counts[0xb000] - ); + // Weight confidences by number of observed hits; if any is above 60% confidence, just use it. + const auto ascii_8kb_total = address_counts[0x6000] + address_counts[0x6800] + address_counts[0x7000] + address_counts[0x7800]; + const auto ascii_16kb_total = address_counts[0x6000] + address_counts[0x7000] + address_counts[0x77ff]; + const auto konami_total = address_counts[0x6000] + address_counts[0x8000] + address_counts[0xa000]; + const auto konami_with_scc_total = address_counts[0x5000] + address_counts[0x7000] + address_counts[0x9000] + address_counts[0xb000]; - targets.push_back(CartridgeTarget( - segment, - start_address, - Analyser::Static::MSX::Cartridge::ASCII8kb, - float( address_counts[0x6000] + - address_counts[0x6800] + - address_counts[0x7000] + - address_counts[0x7800]) / total_hits)); - targets.push_back(CartridgeTarget( - segment, - start_address, - Analyser::Static::MSX::Cartridge::ASCII16kb, - float( address_counts[0x6000] + - address_counts[0x7000] + - address_counts[0x77ff]) / total_hits)); - if(!is_ascii) { + const auto total_hits = ascii_8kb_total + ascii_16kb_total + konami_total + konami_with_scc_total; + + const bool is_ascii_8kb = (ascii_8kb_total * 5) / (total_hits * 3); + const bool is_ascii_16kb = (ascii_16kb_total * 5) / (total_hits * 3); + const bool is_konami = (konami_total * 5) / (total_hits * 3); + const bool is_konami_with_scc = (konami_with_scc_total * 5) / (total_hits * 3); + + if(!is_ascii_16kb && !is_konami && !is_konami_with_scc) { + targets.push_back(CartridgeTarget( + segment, + start_address, + Analyser::Static::MSX::Cartridge::ASCII8kb, + float(ascii_8kb_total) / float(total_hits))); + } + if(!is_ascii_8kb && !is_konami && !is_konami_with_scc) { + targets.push_back(CartridgeTarget( + segment, + start_address, + Analyser::Static::MSX::Cartridge::ASCII16kb, + float(ascii_16kb_total) / float(total_hits))); + } + if(!is_ascii_8kb && !is_ascii_16kb && !is_konami_with_scc) { targets.push_back(CartridgeTarget( segment, start_address, Analyser::Static::MSX::Cartridge::Konami, - float( address_counts[0x6000] + - address_counts[0x8000] + - address_counts[0xa000]) / total_hits)); + float(konami_total) / float(total_hits))); } - if(!is_ascii) { + if(!is_ascii_8kb && !is_ascii_16kb && !is_konami) { targets.push_back(CartridgeTarget( segment, start_address, Analyser::Static::MSX::Cartridge::KonamiWithSCC, - float( address_counts[0x5000] + - address_counts[0x7000] + - address_counts[0x9000] + - address_counts[0xb000]) / total_hits)); + float(konami_with_scc_total) / float(total_hits))); } } diff --git a/Analyser/Static/MSX/StaticAnalyser.hpp b/Analyser/Static/MSX/StaticAnalyser.hpp index e60115bb1..1fa329dd7 100644 --- a/Analyser/Static/MSX/StaticAnalyser.hpp +++ b/Analyser/Static/MSX/StaticAnalyser.hpp @@ -6,21 +6,14 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef StaticAnalyser_MSX_StaticAnalyser_hpp -#define StaticAnalyser_MSX_StaticAnalyser_hpp +#pragma once #include "../StaticAnalyser.hpp" #include "../../../Storage/TargetPlatforms.hpp" #include -namespace Analyser { -namespace Static { -namespace MSX { +namespace Analyser::Static::MSX { TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); } -} -} - -#endif /* StaticAnalyser_MSX_StaticAnalyser_hpp */ diff --git a/Analyser/Static/MSX/Tape.hpp b/Analyser/Static/MSX/Tape.hpp index eb0ca66bf..47bc1810d 100644 --- a/Analyser/Static/MSX/Tape.hpp +++ b/Analyser/Static/MSX/Tape.hpp @@ -6,17 +6,14 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef StaticAnalyser_MSX_Tape_hpp -#define StaticAnalyser_MSX_Tape_hpp +#pragma once #include "../../../Storage/Tape/Tape.hpp" #include #include -namespace Analyser { -namespace Static { -namespace MSX { +namespace Analyser::Static::MSX { struct File { std::string name; @@ -38,7 +35,3 @@ struct File { std::vector GetFiles(const std::shared_ptr &tape); } -} -} - -#endif /* StaticAnalyser_MSX_Tape_hpp */ diff --git a/Analyser/Static/MSX/Target.hpp b/Analyser/Static/MSX/Target.hpp index f0d7a9f6a..a322ce83c 100644 --- a/Analyser/Static/MSX/Target.hpp +++ b/Analyser/Static/MSX/Target.hpp @@ -6,22 +6,26 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_MSX_Target_h -#define Analyser_Static_MSX_Target_h +#pragma once #include "../../../Reflection/Enum.hpp" #include "../../../Reflection/Struct.hpp" #include "../StaticAnalyser.hpp" #include -namespace Analyser { -namespace Static { -namespace MSX { +namespace Analyser::Static::MSX { struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl { bool has_disk_drive = false; + bool has_msx_music = true; std::string loading_command; + ReflectableEnum(Model, + MSX1, + MSX2 + ); + Model model = Model::MSX2; + ReflectableEnum(Region, Japan, USA, @@ -32,14 +36,13 @@ struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl< Target(): Analyser::Static::Target(Machine::MSX) { if(needs_declare()) { DeclareField(has_disk_drive); + DeclareField(has_msx_music); DeclareField(region); AnnounceEnum(Region); + DeclareField(model); + AnnounceEnum(Model); } } }; } -} -} - -#endif /* Analyser_Static_MSX_Target_h */ diff --git a/Analyser/Static/Macintosh/StaticAnalyser.hpp b/Analyser/Static/Macintosh/StaticAnalyser.hpp index 8b40c0120..bf8924246 100644 --- a/Analyser/Static/Macintosh/StaticAnalyser.hpp +++ b/Analyser/Static/Macintosh/StaticAnalyser.hpp @@ -6,22 +6,14 @@ // Copyright © 2019 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_Macintosh_StaticAnalyser_hpp -#define Analyser_Static_Macintosh_StaticAnalyser_hpp +#pragma once #include "../StaticAnalyser.hpp" #include "../../../Storage/TargetPlatforms.hpp" #include -namespace Analyser { -namespace Static { -namespace Macintosh { +namespace Analyser::Static::Macintosh { TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); } -} -} - - -#endif /* Analyser_Static_Macintosh_StaticAnalyser_hpp */ diff --git a/Analyser/Static/Macintosh/Target.hpp b/Analyser/Static/Macintosh/Target.hpp index f00cb4a24..372d212bf 100644 --- a/Analyser/Static/Macintosh/Target.hpp +++ b/Analyser/Static/Macintosh/Target.hpp @@ -6,16 +6,13 @@ // Copyright © 2019 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_Macintosh_Target_h -#define Analyser_Static_Macintosh_Target_h +#pragma once #include "../../../Reflection/Enum.hpp" #include "../../../Reflection/Struct.hpp" #include "../StaticAnalyser.hpp" -namespace Analyser { -namespace Static { -namespace Macintosh { +namespace Analyser::Static::Macintosh { struct Target: public Analyser::Static::Target, public Reflection::StructImpl { ReflectableEnum(Model, Mac128k, Mac512k, Mac512ke, MacPlus); @@ -31,7 +28,3 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImplsamples.empty()) return false; @@ -108,7 +108,7 @@ bool is_400_loader(Storage::Encodings::MFM::Parser &parser, uint16_t range_start use disassembly to test for likely matches. */ - Storage::Encodings::MFM::Sector *sector = parser.get_sector(0, 0, 1); + const Storage::Encodings::MFM::Sector *sector = parser.sector(0, 0, 1); if(!sector) return false; if(sector->samples.empty()) return false; @@ -175,7 +175,7 @@ Analyser::Static::TargetList Analyser::Static::Oric::GetTargets(const Media &med // 8-DOS is recognised by a dedicated Disk II analyser, so check only for Microdisc, // Jasmin and BD-DOS formats here. for(auto &disk: media.disks) { - Storage::Encodings::MFM::Parser parser(true, disk); + Storage::Encodings::MFM::Parser parser(Storage::Encodings::MFM::Density::Double, disk); if(is_microdisc(parser)) { target->disk_interface = Target::DiskInterface::Microdisc; diff --git a/Analyser/Static/Oric/StaticAnalyser.hpp b/Analyser/Static/Oric/StaticAnalyser.hpp index f97d84772..dadd90834 100644 --- a/Analyser/Static/Oric/StaticAnalyser.hpp +++ b/Analyser/Static/Oric/StaticAnalyser.hpp @@ -6,21 +6,14 @@ // Copyright 2016 Thomas Harte. All rights reserved. // -#ifndef StaticAnalyser_Oric_StaticAnalyser_hpp -#define StaticAnalyser_Oric_StaticAnalyser_hpp +#pragma once #include "../StaticAnalyser.hpp" #include "../../../Storage/TargetPlatforms.hpp" #include -namespace Analyser { -namespace Static { -namespace Oric { +namespace Analyser::Static::Oric { TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); } -} -} - -#endif /* StaticAnalyser_hpp */ diff --git a/Analyser/Static/Oric/Tape.hpp b/Analyser/Static/Oric/Tape.hpp index fc7734697..a4bafbdfb 100644 --- a/Analyser/Static/Oric/Tape.hpp +++ b/Analyser/Static/Oric/Tape.hpp @@ -6,17 +6,14 @@ // Copyright 2016 Thomas Harte. All rights reserved. // -#ifndef StaticAnalyser_Oric_Tape_hpp -#define StaticAnalyser_Oric_Tape_hpp +#pragma once #include "../../../Storage/Tape/Tape.hpp" #include #include -namespace Analyser { -namespace Static { -namespace Oric { +namespace Analyser::Static::Oric { struct File { std::string name; @@ -34,7 +31,3 @@ struct File { std::vector GetFiles(const std::shared_ptr &tape); } -} -} - -#endif /* Tape_hpp */ diff --git a/Analyser/Static/Oric/Target.hpp b/Analyser/Static/Oric/Target.hpp index 74e686dff..28bfcc598 100644 --- a/Analyser/Static/Oric/Target.hpp +++ b/Analyser/Static/Oric/Target.hpp @@ -6,17 +6,14 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_Oric_Target_h -#define Analyser_Static_Oric_Target_h +#pragma once #include "../../../Reflection/Enum.hpp" #include "../../../Reflection/Struct.hpp" #include "../StaticAnalyser.hpp" #include -namespace Analyser { -namespace Static { -namespace Oric { +namespace Analyser::Static::Oric { struct Target: public Analyser::Static::Target, public Reflection::StructImpl { ReflectableEnum(ROM, @@ -57,7 +54,3 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImplmedia = media; + targets.push_back(std::unique_ptr(target)); + + return targets; +} diff --git a/Analyser/Static/PCCompatible/StaticAnalyser.hpp b/Analyser/Static/PCCompatible/StaticAnalyser.hpp new file mode 100644 index 000000000..c7a52373e --- /dev/null +++ b/Analyser/Static/PCCompatible/StaticAnalyser.hpp @@ -0,0 +1,19 @@ +// +// StaticAnalyser.hpp +// Clock Signal +// +// Created by Thomas Harte on 29/11/2019. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "../StaticAnalyser.hpp" +#include "../../../Storage/TargetPlatforms.hpp" +#include + +namespace Analyser::Static::PCCompatible { + +TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); + +} diff --git a/Analyser/Static/PCCompatible/Target.hpp b/Analyser/Static/PCCompatible/Target.hpp new file mode 100644 index 000000000..5c954306f --- /dev/null +++ b/Analyser/Static/PCCompatible/Target.hpp @@ -0,0 +1,37 @@ +// +// Target.hpp +// Clock Signal +// +// Created by Thomas Harte on 29/11/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "../../../Reflection/Struct.hpp" +#include "../StaticAnalyser.hpp" + +namespace Analyser::Static::PCCompatible { + +struct Target: public Analyser::Static::Target, public Reflection::StructImpl { + ReflectableEnum(VideoAdaptor, + MDA, + CGA); + VideoAdaptor adaptor = VideoAdaptor::CGA; + + ReflectableEnum(Speed, + ApproximatelyOriginal, + Fast); + Speed speed = Speed::Fast; + + Target() : Analyser::Static::Target(Machine::PCCompatible) { + if(needs_declare()) { + AnnounceEnum(VideoAdaptor); + AnnounceEnum(Speed); + DeclareField(adaptor); + DeclareField(speed); + } + } +}; + +} diff --git a/Analyser/Static/Sega/StaticAnalyser.hpp b/Analyser/Static/Sega/StaticAnalyser.hpp index 127e2cd8d..6840cefb2 100644 --- a/Analyser/Static/Sega/StaticAnalyser.hpp +++ b/Analyser/Static/Sega/StaticAnalyser.hpp @@ -6,21 +6,14 @@ // Copyright © 2018 Thomas Harte. All rights reserved. // -#ifndef StaticAnalyser_Sega_StaticAnalyser_hpp -#define StaticAnalyser_Sega_StaticAnalyser_hpp +#pragma once #include "../StaticAnalyser.hpp" #include "../../../Storage/TargetPlatforms.hpp" #include -namespace Analyser { -namespace Static { -namespace Sega { +namespace Analyser::Static::Sega { TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); } -} -} - -#endif /* StaticAnalyser_hpp */ diff --git a/Analyser/Static/Sega/Target.hpp b/Analyser/Static/Sega/Target.hpp index 3eceb774d..74653f012 100644 --- a/Analyser/Static/Sega/Target.hpp +++ b/Analyser/Static/Sega/Target.hpp @@ -6,16 +6,13 @@ // Copyright © 2018 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_Sega_Target_h -#define Analyser_Static_Sega_Target_h +#pragma once #include "../../../Reflection/Enum.hpp" #include "../../../Reflection/Struct.hpp" #include "../StaticAnalyser.hpp" -namespace Analyser { -namespace Static { -namespace Sega { +namespace Analyser::Static::Sega { struct Target: public Analyser::Static::Target, public Reflection::StructImpl { enum class Model { @@ -48,10 +45,8 @@ struct Target: public Analyser::Static::Target, public Reflection::StructImpl= Analyser::Static::Sega::Target::Model::MasterSystem - -} -} +constexpr bool is_master_system(Analyser::Static::Sega::Target::Model model) { + return model >= Analyser::Static::Sega::Target::Model::MasterSystem; } -#endif /* Analyser_Static_Sega_Target_h */ +} diff --git a/Analyser/Static/StaticAnalyser.cpp b/Analyser/Static/StaticAnalyser.cpp index 88167f26c..d57749c1e 100644 --- a/Analyser/Static/StaticAnalyser.cpp +++ b/Analyser/Static/StaticAnalyser.cpp @@ -9,6 +9,7 @@ #include "StaticAnalyser.hpp" #include +#include #include #include #include @@ -25,9 +26,11 @@ #include "Commodore/StaticAnalyser.hpp" #include "DiskII/StaticAnalyser.hpp" #include "Enterprise/StaticAnalyser.hpp" +#include "FAT12/StaticAnalyser.hpp" #include "Macintosh/StaticAnalyser.hpp" #include "MSX/StaticAnalyser.hpp" #include "Oric/StaticAnalyser.hpp" +#include "PCCompatible/StaticAnalyser.hpp" #include "Sega/StaticAnalyser.hpp" #include "ZX8081/StaticAnalyser.hpp" #include "ZXSpectrum/StaticAnalyser.hpp" @@ -48,10 +51,12 @@ #include "../../Storage/Disk/DiskImage/Formats/FAT12.hpp" #include "../../Storage/Disk/DiskImage/Formats/HFE.hpp" #include "../../Storage/Disk/DiskImage/Formats/IPF.hpp" +#include "../../Storage/Disk/DiskImage/Formats/IMD.hpp" #include "../../Storage/Disk/DiskImage/Formats/MacintoshIMG.hpp" #include "../../Storage/Disk/DiskImage/Formats/MSA.hpp" #include "../../Storage/Disk/DiskImage/Formats/NIB.hpp" #include "../../Storage/Disk/DiskImage/Formats/OricMFMDSK.hpp" +#include "../../Storage/Disk/DiskImage/Formats/PCBooter.hpp" #include "../../Storage/Disk/DiskImage/Formats/SSD.hpp" #include "../../Storage/Disk/DiskImage/Formats/STX.hpp" #include "../../Storage/Disk/DiskImage/Formats/WOZ.hpp" @@ -59,6 +64,7 @@ // Mass Storage Devices (i.e. usually, hard disks) #include "../../Storage/MassStorage/Formats/DAT.hpp" #include "../../Storage/MassStorage/Formats/DSK.hpp" +#include "../../Storage/MassStorage/Formats/HDV.hpp" #include "../../Storage/MassStorage/Formats/HFV.hpp" // State Snapshots @@ -83,6 +89,7 @@ template inline constexpr bool always_false_v = false; using namespace Analyser::Static; +using namespace Storage; namespace { @@ -96,46 +103,90 @@ std::string get_extension(const std::string &name) { return extension; } +class MediaAccumulator { + public: + MediaAccumulator(const std::string &file_name, TargetPlatform::IntType &potential_platforms) : + file_name_(file_name), potential_platforms_(potential_platforms), extension_(get_extension(file_name)) {} + + /// Adds @c instance to the media collection and adds @c platforms to the set of potentials. + /// If @c instance is an @c TargetPlatform::TypeDistinguisher then it is given an opportunity to restrict the set of potentials. + template + void insert(TargetPlatform::IntType platforms, std::shared_ptr instance) { + if constexpr (std::is_base_of_v) { + media.disks.push_back(instance); + } else if constexpr (std::is_base_of_v) { + media.tapes.push_back(instance); + } else if constexpr (std::is_base_of_v) { + media.cartridges.push_back(instance); + } else if constexpr (std::is_base_of_v) { + media.mass_storage_devices.push_back(instance); + } else { + static_assert(always_false_v, "Unexpected type encountered."); + } + + potential_platforms_ |= platforms; + + // Check whether the instance itself has any input on target platforms. + TargetPlatform::TypeDistinguisher *const distinguisher = + dynamic_cast(instance.get()); + if(distinguisher) potential_platforms_ &= distinguisher->target_platform_type(); + } + + /// Concstructs a new instance of @c InstanceT supplying @c args and adds it to the back of @c list using @c insert_instance. + template + void insert(TargetPlatform::IntType platforms, Args &&... args) { + insert(platforms, std::make_shared(std::forward(args)...)); + } + + /// Calls @c insert with the specified parameters, ignoring any exceptions thrown. + template + void try_insert(TargetPlatform::IntType platforms, Args &&... args) { + try { + insert(platforms, std::forward(args)...); + } catch(...) {} + } + + /// Performs a @c try_insert for an object of @c InstanceT if @c extension matches that of the file name, + /// providing the file name as the only construction argument. + template + void try_standard(TargetPlatform::IntType platforms, const char *extension) { + if(name_matches(extension)) { + try_insert(platforms, file_name_); + } + } + + bool name_matches(const char *extension) { + return extension_ == extension; + } + + Media media; + + private: + const std::string &file_name_; + TargetPlatform::IntType &potential_platforms_; + const std::string extension_; +}; + } static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform::IntType &potential_platforms) { - Media result; - const std::string extension = get_extension(file_name); - -#define InsertInstance(list, instance, platforms) \ - list.emplace_back(instance);\ - potential_platforms |= platforms;\ - TargetPlatform::TypeDistinguisher *const distinguisher = dynamic_cast(list.back().get());\ - if(distinguisher) potential_platforms &= distinguisher->target_platform_type(); - -#define Insert(list, class, platforms, ...) \ - InsertInstance(list, new Storage::class(__VA_ARGS__), platforms); - -#define TryInsert(list, class, platforms, ...) \ - try {\ - Insert(list, class, platforms, __VA_ARGS__) \ - } catch(...) {} - -#define Format(ext, list, class, platforms) \ - if(extension == ext) { \ - TryInsert(list, class, platforms, file_name) \ - } + MediaAccumulator accumulator(file_name, potential_platforms); // 2MG - if(extension == "2mg") { + if(accumulator.name_matches("2mg")) { // 2MG uses a factory method; defer to it. try { - const auto media = Storage::Disk::Disk2MG::open(file_name); - std::visit([&result, &potential_platforms](auto &&arg) { + const auto media = Disk::Disk2MG::open(file_name); + std::visit([&](auto &&arg) { using Type = typename std::decay::type; - if constexpr (std::is_same::value) { + if constexpr (std::is_same::value) { // It's valid for no media to be returned. - } else if constexpr (std::is_same::value) { - InsertInstance(result.disks, arg, TargetPlatform::DiskII); - } else if constexpr (std::is_same::value) { + } else if constexpr (std::is_same::value) { + accumulator.insert(TargetPlatform::DiskII, std::shared_ptr(arg)); + } else if constexpr (std::is_same::value) { // TODO: or is it Apple IIgs? - InsertInstance(result.mass_storage_devices, arg, TargetPlatform::AppleII); + accumulator.insert(TargetPlatform::AppleII, std::shared_ptr(arg)); } else { static_assert(always_false_v, "Unexpected type encountered."); } @@ -143,93 +194,108 @@ static Media GetMediaAndPlatforms(const std::string &file_name, TargetPlatform:: } catch(...) {} } - Format("80", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // 80 - Format("81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // 81 - Format("a26", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Atari2600) // A26 - Format("adf", result.disks, Disk::DiskImageHolder, TargetPlatform::Acorn) // ADF (Acorn) - Format("adf", result.disks, Disk::DiskImageHolder, TargetPlatform::Amiga) // ADF (Amiga) - Format("adl", result.disks, Disk::DiskImageHolder, TargetPlatform::Acorn) // ADL - Format("bin", result.cartridges, Cartridge::BinaryDump, TargetPlatform::AllCartridge) // BIN (cartridge dump) - Format("cas", result.tapes, Tape::CAS, TargetPlatform::MSX) // CAS - Format("cdt", result.tapes, Tape::TZX, TargetPlatform::AmstradCPC) // CDT - Format("col", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Coleco) // COL - Format("csw", result.tapes, Tape::CSW, TargetPlatform::AllTape) // CSW - Format("d64", result.disks, Disk::DiskImageHolder, TargetPlatform::Commodore) // D64 - Format("dat", result.mass_storage_devices, MassStorage::DAT, TargetPlatform::Acorn) // DAT - Format("dmk", result.disks, Disk::DiskImageHolder, TargetPlatform::MSX) // DMK - Format("do", result.disks, Disk::DiskImageHolder, TargetPlatform::DiskII) // DO - Format("dsd", result.disks, Disk::DiskImageHolder, TargetPlatform::Acorn) // DSD - Format( "dsk", - result.disks, - Disk::DiskImageHolder, - TargetPlatform::AmstradCPC | TargetPlatform::Oric | TargetPlatform::ZXSpectrum) // DSK (Amstrad CPC, etc) - Format("dsk", result.disks, Disk::DiskImageHolder, TargetPlatform::DiskII) // DSK (Apple II) - Format("dsk", result.disks, Disk::DiskImageHolder, TargetPlatform::Macintosh) // DSK (Macintosh, floppy disk) - Format("dsk", result.mass_storage_devices, MassStorage::HFV, TargetPlatform::Macintosh) // DSK (Macintosh, hard disk, single volume image) - Format("dsk", result.mass_storage_devices, MassStorage::DSK, TargetPlatform::Macintosh) // DSK (Macintosh, hard disk, full device image) - Format("dsk", result.disks, Disk::DiskImageHolder, TargetPlatform::MSX) // DSK (MSX) - Format("dsk", result.disks, Disk::DiskImageHolder, TargetPlatform::Oric) // DSK (Oric) - Format("g64", result.disks, Disk::DiskImageHolder, TargetPlatform::Commodore) // G64 - Format( "hfe", - result.disks, - Disk::DiskImageHolder, - TargetPlatform::Acorn | TargetPlatform::AmstradCPC | TargetPlatform::Commodore | TargetPlatform::Oric | TargetPlatform::ZXSpectrum) - // HFE (TODO: switch to AllDisk once the MSX stops being so greedy) - Format("img", result.disks, Disk::DiskImageHolder, TargetPlatform::Macintosh) // IMG (DiskCopy 4.2) - Format("image", result.disks, Disk::DiskImageHolder, TargetPlatform::Macintosh) // IMG (DiskCopy 4.2) - Format("img", result.disks, Disk::DiskImageHolder, TargetPlatform::Enterprise) // IMG (Enterprise/MS-DOS style) - Format( "ipf", - result.disks, - Disk::DiskImageHolder, - TargetPlatform::Amiga | TargetPlatform::AtariST | TargetPlatform::AmstradCPC | TargetPlatform::ZXSpectrum) // IPF - Format("msa", result.disks, Disk::DiskImageHolder, TargetPlatform::AtariST) // MSA - Format("nib", result.disks, Disk::DiskImageHolder, TargetPlatform::DiskII) // NIB - Format("o", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // O - Format("p", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // P - Format("po", result.disks, Disk::DiskImageHolder, TargetPlatform::DiskII) // PO (original Apple II kind) + accumulator.try_standard(TargetPlatform::ZX8081, "80"); + accumulator.try_standard(TargetPlatform::ZX8081, "81"); - // PO (Apple IIgs kind) - if(extension == "po") { - TryInsert(result.disks, Disk::DiskImageHolder, TargetPlatform::AppleIIgs, file_name, Storage::Disk::MacintoshIMG::FixedType::GCR) + accumulator.try_standard(TargetPlatform::Atari2600, "a26"); + accumulator.try_standard>(TargetPlatform::Acorn, "adf"); + accumulator.try_standard>(TargetPlatform::Amiga, "adf"); + accumulator.try_standard>(TargetPlatform::Acorn, "adl"); + + accumulator.try_standard(TargetPlatform::AllCartridge, "bin"); + + accumulator.try_standard(TargetPlatform::MSX, "cas"); + accumulator.try_standard(TargetPlatform::AmstradCPC, "cdt"); + accumulator.try_standard(TargetPlatform::Coleco, "col"); + accumulator.try_standard(TargetPlatform::AllTape, "csw"); + + accumulator.try_standard>(TargetPlatform::Commodore, "d64"); + accumulator.try_standard(TargetPlatform::Acorn, "dat"); + accumulator.try_standard>(TargetPlatform::MSX, "dmk"); + accumulator.try_standard>(TargetPlatform::DiskII, "do"); + accumulator.try_standard>(TargetPlatform::Acorn, "dsd"); + accumulator.try_standard>( + TargetPlatform::AmstradCPC | TargetPlatform::Oric | TargetPlatform::ZXSpectrum, "dsk"); + accumulator.try_standard>(TargetPlatform::DiskII, "dsk"); + accumulator.try_standard>(TargetPlatform::Macintosh, "dsk"); + accumulator.try_standard(TargetPlatform::Macintosh, "dsk"); + accumulator.try_standard(TargetPlatform::Macintosh, "dsk"); + accumulator.try_standard>(TargetPlatform::MSX, "dsk"); + accumulator.try_standard>(TargetPlatform::Oric, "dsk"); + + accumulator.try_standard>(TargetPlatform::Commodore, "g64"); + + accumulator.try_standard(TargetPlatform::AppleII, "hdv"); + accumulator.try_standard>( + TargetPlatform::Acorn | TargetPlatform::AmstradCPC | TargetPlatform::Commodore | TargetPlatform::Oric | TargetPlatform::ZXSpectrum, + "hfe"); // TODO: switch to AllDisk once the MSX stops being so greedy. + + accumulator.try_standard>(TargetPlatform::PCCompatible, "ima"); + accumulator.try_standard>(TargetPlatform::Macintosh, "image"); + accumulator.try_standard>(TargetPlatform::PCCompatible, "imd"); + accumulator.try_standard>(TargetPlatform::Macintosh, "img"); + + // Treat PC booter as a potential backup only if this doesn't parse as a FAT12. + if(accumulator.name_matches("img")) { + try { + accumulator.insert>(TargetPlatform::FAT12, file_name); + } catch(...) { + accumulator.try_standard>(TargetPlatform::PCCompatible, "img"); + } } - Format("p81", result.tapes, Tape::ZX80O81P, TargetPlatform::ZX8081) // P81 + accumulator.try_standard>( + TargetPlatform::Amiga | TargetPlatform::AtariST | TargetPlatform::AmstradCPC | TargetPlatform::ZXSpectrum, + "ipf"); - // PRG - if(extension == "prg") { - // try instantiating as a ROM; failing that accept as a tape + accumulator.try_standard>(TargetPlatform::AtariST, "msa"); + accumulator.try_standard(TargetPlatform::MSX, "mx2"); + accumulator.try_standard>(TargetPlatform::DiskII, "nib"); + + accumulator.try_standard(TargetPlatform::ZX8081, "o"); + accumulator.try_standard(TargetPlatform::ZX8081, "p"); + accumulator.try_standard>(TargetPlatform::DiskII, "po"); + + if(accumulator.name_matches("po")) { + accumulator.try_insert>( + TargetPlatform::AppleIIgs, + file_name, Disk::MacintoshIMG::FixedType::GCR); + } + + accumulator.try_standard(TargetPlatform::ZX8081, "p81"); + + if(accumulator.name_matches("prg")) { + // Try instantiating as a ROM; failing that accept as a tape. try { - Insert(result.cartridges, Cartridge::PRG, TargetPlatform::Commodore, file_name) + accumulator.insert(TargetPlatform::Commodore, file_name); } catch(...) { try { - Insert(result.tapes, Tape::PRG, TargetPlatform::Commodore, file_name) + accumulator.insert(TargetPlatform::Commodore, file_name); } catch(...) {} } } - Format( "rom", - result.cartridges, - Cartridge::BinaryDump, - TargetPlatform::AcornElectron | TargetPlatform::Coleco | TargetPlatform::MSX) // ROM - Format("sg", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega) // SG - Format("sms", result.cartridges, Cartridge::BinaryDump, TargetPlatform::Sega) // SMS - Format("ssd", result.disks, Disk::DiskImageHolder, TargetPlatform::Acorn) // SSD - Format("st", result.disks, Disk::DiskImageHolder, TargetPlatform::AtariST) // ST - Format("stx", result.disks, Disk::DiskImageHolder, TargetPlatform::AtariST) // STX - Format("tap", result.tapes, Tape::CommodoreTAP, TargetPlatform::Commodore) // TAP (Commodore) - Format("tap", result.tapes, Tape::OricTAP, TargetPlatform::Oric) // TAP (Oric) - Format("tap", result.tapes, Tape::ZXSpectrumTAP, TargetPlatform::ZXSpectrum) // TAP (ZX Spectrum) - Format("tsx", result.tapes, Tape::TZX, TargetPlatform::MSX) // TSX - Format("tzx", result.tapes, Tape::TZX, TargetPlatform::ZX8081 | TargetPlatform::ZXSpectrum) // TZX - Format("uef", result.tapes, Tape::UEF, TargetPlatform::Acorn) // UEF (tape) - Format("woz", result.disks, Disk::DiskImageHolder, TargetPlatform::DiskII) // WOZ + accumulator.try_standard( + TargetPlatform::AcornElectron | TargetPlatform::Coleco | TargetPlatform::MSX, + "rom"); -#undef Format -#undef Insert -#undef TryInsert -#undef InsertInstance + accumulator.try_standard(TargetPlatform::Sega, "sg"); + accumulator.try_standard(TargetPlatform::Sega, "sms"); + accumulator.try_standard>(TargetPlatform::Acorn, "ssd"); + accumulator.try_standard>(TargetPlatform::AtariST, "st"); + accumulator.try_standard>(TargetPlatform::AtariST, "stx"); - return result; + accumulator.try_standard(TargetPlatform::Commodore, "tap"); + accumulator.try_standard(TargetPlatform::Oric, "tap"); + accumulator.try_standard(TargetPlatform::ZXSpectrum, "tap"); + accumulator.try_standard(TargetPlatform::MSX, "tsx"); + accumulator.try_standard(TargetPlatform::ZX8081 | TargetPlatform::ZXSpectrum, "tzx"); + + accumulator.try_standard(TargetPlatform::Acorn, "uef"); + + accumulator.try_standard>(TargetPlatform::DiskII, "woz"); + + return accumulator.media; } Media Analyser::Static::GetMedia(const std::string &file_name) { @@ -238,26 +304,28 @@ Media Analyser::Static::GetMedia(const std::string &file_name) { } TargetList Analyser::Static::GetTargets(const std::string &file_name) { - TargetList targets; const std::string extension = get_extension(file_name); + TargetList targets; // Check whether the file directly identifies a target; if so then just return that. -#define Format(ext, class) \ - if(extension == ext) { \ - try { \ - auto target = Storage::State::class::load(file_name); \ - if(target) { \ - targets.push_back(std::move(target)); \ - return targets; \ - } \ - } catch(...) {} \ - } + const auto try_snapshot = [&](const char *ext, auto loader) -> bool { + if(extension != ext) { + return false; + } + try { + auto target = loader(file_name); + if(target) { + targets.push_back(std::move(target)); + return true; + } + } catch(...) {} - Format("sna", SNA); - Format("szx", SZX); - Format("z80", Z80); + return false; + }; -#undef TryInsert + if(try_snapshot("sna", Storage::State::SNA::load)) return targets; + if(try_snapshot("szx", Storage::State::SZX::load)) return targets; + if(try_snapshot("z80", Storage::State::Z80::load)) return targets; // Otherwise: // @@ -268,28 +336,33 @@ TargetList Analyser::Static::GetTargets(const std::string &file_name) { // Hand off to platform-specific determination of whether these // things are actually compatible and, if so, how to load them. -#define Append(x) if(potential_platforms & TargetPlatform::x) {\ - auto new_targets = x::GetTargets(media, file_name, potential_platforms);\ - std::move(new_targets.begin(), new_targets.end(), std::back_inserter(targets));\ -} - Append(Acorn); - Append(AmstradCPC); - Append(AppleII); - Append(AppleIIgs); - Append(Amiga); - Append(Atari2600); - Append(AtariST); - Append(Coleco); - Append(Commodore); - Append(DiskII); - Append(Enterprise); - Append(Macintosh); - Append(MSX); - Append(Oric); - Append(Sega); - Append(ZX8081); - Append(ZXSpectrum); -#undef Append + const auto append = [&](TargetPlatform::IntType platform, auto evaluator) { + if(!(potential_platforms & platform)) { + return; + } + auto new_targets = evaluator(media, file_name, potential_platforms); + std::move(new_targets.begin(), new_targets.end(), std::back_inserter(targets)); + }; + + append(TargetPlatform::Acorn, Acorn::GetTargets); + append(TargetPlatform::AmstradCPC, AmstradCPC::GetTargets); + append(TargetPlatform::AppleII, AppleII::GetTargets); + append(TargetPlatform::AppleIIgs, AppleIIgs::GetTargets); + append(TargetPlatform::Amiga, Amiga::GetTargets); + append(TargetPlatform::Atari2600, Atari2600::GetTargets); + append(TargetPlatform::AtariST, AtariST::GetTargets); + append(TargetPlatform::Coleco, Coleco::GetTargets); + append(TargetPlatform::Commodore, Commodore::GetTargets); + append(TargetPlatform::DiskII, DiskII::GetTargets); + append(TargetPlatform::Enterprise, Enterprise::GetTargets); + append(TargetPlatform::FAT12, FAT12::GetTargets); + append(TargetPlatform::Macintosh, Macintosh::GetTargets); + append(TargetPlatform::MSX, MSX::GetTargets); + append(TargetPlatform::Oric, Oric::GetTargets); + append(TargetPlatform::PCCompatible, PCCompatible::GetTargets); + append(TargetPlatform::Sega, Sega::GetTargets); + append(TargetPlatform::ZX8081, ZX8081::GetTargets); + append(TargetPlatform::ZXSpectrum, ZXSpectrum::GetTargets); // Reset any tapes to their initial position. for(const auto &target : targets) { diff --git a/Analyser/Static/StaticAnalyser.hpp b/Analyser/Static/StaticAnalyser.hpp index 259f7cca9..63b9e8867 100644 --- a/Analyser/Static/StaticAnalyser.hpp +++ b/Analyser/Static/StaticAnalyser.hpp @@ -6,8 +6,7 @@ // Copyright 2016 Thomas Harte. All rights reserved. // -#ifndef StaticAnalyser_hpp -#define StaticAnalyser_hpp +#pragma once #include "../Machines.hpp" @@ -21,8 +20,7 @@ #include #include -namespace Analyser { -namespace Static { +namespace Analyser::Static { struct State; @@ -80,6 +78,3 @@ TargetList GetTargets(const std::string &file_name); Media GetMedia(const std::string &file_name); } -} - -#endif /* StaticAnalyser_hpp */ diff --git a/Analyser/Static/ZX8081/StaticAnalyser.hpp b/Analyser/Static/ZX8081/StaticAnalyser.hpp index 141a15945..bac9d9084 100644 --- a/Analyser/Static/ZX8081/StaticAnalyser.hpp +++ b/Analyser/Static/ZX8081/StaticAnalyser.hpp @@ -6,21 +6,14 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_ZX8081_StaticAnalyser_hpp -#define Analyser_Static_ZX8081_StaticAnalyser_hpp +#pragma once #include "../StaticAnalyser.hpp" #include "../../../Storage/TargetPlatforms.hpp" #include -namespace Analyser { -namespace Static { -namespace ZX8081 { +namespace Analyser::Static::ZX8081 { TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); } -} -} - -#endif /* StaticAnalyser_hpp */ diff --git a/Analyser/Static/ZX8081/Target.hpp b/Analyser/Static/ZX8081/Target.hpp index c7dd7e60d..2f83bbdb1 100644 --- a/Analyser/Static/ZX8081/Target.hpp +++ b/Analyser/Static/ZX8081/Target.hpp @@ -6,17 +6,14 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_ZX8081_Target_h -#define Analyser_Static_ZX8081_Target_h +#pragma once #include "../../../Reflection/Enum.hpp" #include "../../../Reflection/Struct.hpp" #include "../StaticAnalyser.hpp" #include -namespace Analyser { -namespace Static { -namespace ZX8081 { +namespace Analyser::Static::ZX8081 { struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl { ReflectableEnum(MemoryModel, @@ -41,7 +38,3 @@ struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl< }; } -} -} - -#endif /* Analyser_Static_ZX8081_Target_h */ diff --git a/Analyser/Static/ZXSpectrum/StaticAnalyser.cpp b/Analyser/Static/ZXSpectrum/StaticAnalyser.cpp index 0c21161e8..396c67f23 100644 --- a/Analyser/Static/ZXSpectrum/StaticAnalyser.cpp +++ b/Analyser/Static/ZXSpectrum/StaticAnalyser.cpp @@ -33,14 +33,14 @@ bool IsSpectrumTape(const std::shared_ptr &tape) { } bool IsSpectrumDisk(const std::shared_ptr &disk) { - Storage::Encodings::MFM::Parser parser(true, disk); + Storage::Encodings::MFM::Parser parser(Storage::Encodings::MFM::Density::Double, disk); // Get logical sector 1; the Spectrum appears to support various physical // sectors as sector 1. - Storage::Encodings::MFM::Sector *boot_sector = nullptr; + const Storage::Encodings::MFM::Sector *boot_sector = nullptr; uint8_t sector_mask = 0; while(!boot_sector) { - boot_sector = parser.get_sector(0, 0, sector_mask + 1); + boot_sector = parser.sector(0, 0, sector_mask + 1); sector_mask += 0x40; if(!sector_mask) break; } diff --git a/Analyser/Static/ZXSpectrum/StaticAnalyser.hpp b/Analyser/Static/ZXSpectrum/StaticAnalyser.hpp index 756c28ad0..3e48fe32b 100644 --- a/Analyser/Static/ZXSpectrum/StaticAnalyser.hpp +++ b/Analyser/Static/ZXSpectrum/StaticAnalyser.hpp @@ -6,21 +6,14 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_ZXSpectrum_StaticAnalyser_hpp -#define Analyser_Static_ZXSpectrum_StaticAnalyser_hpp +#pragma once #include "../StaticAnalyser.hpp" #include "../../../Storage/TargetPlatforms.hpp" #include -namespace Analyser { -namespace Static { -namespace ZXSpectrum { +namespace Analyser::Static::ZXSpectrum { TargetList GetTargets(const Media &media, const std::string &file_name, TargetPlatform::IntType potential_platforms); } -} -} - -#endif /* StaticAnalyser_hpp */ diff --git a/Analyser/Static/ZXSpectrum/Target.hpp b/Analyser/Static/ZXSpectrum/Target.hpp index 76eada20e..72ee008d1 100644 --- a/Analyser/Static/ZXSpectrum/Target.hpp +++ b/Analyser/Static/ZXSpectrum/Target.hpp @@ -6,16 +6,13 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef Analyser_Static_ZXSpectrum_Target_h -#define Analyser_Static_ZXSpectrum_Target_h +#pragma once #include "../../../Reflection/Enum.hpp" #include "../../../Reflection/Struct.hpp" #include "../StaticAnalyser.hpp" -namespace Analyser { -namespace Static { -namespace ZXSpectrum { +namespace Analyser::Static::ZXSpectrum { struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl { ReflectableEnum(Model, @@ -39,7 +36,3 @@ struct Target: public ::Analyser::Static::Target, public Reflection::StructImpl< }; } -} -} - -#endif /* Target_h */ diff --git a/ClockReceiver/ClockReceiver.hpp b/ClockReceiver/ClockReceiver.hpp index 907eef9fa..340341435 100644 --- a/ClockReceiver/ClockReceiver.hpp +++ b/ClockReceiver/ClockReceiver.hpp @@ -6,8 +6,7 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef ClockReceiver_hpp -#define ClockReceiver_hpp +#pragma once #include "ForceInline.hpp" @@ -277,5 +276,3 @@ template class HalfClockReceiver: public T { private: HalfCycles half_cycles_; }; - -#endif /* ClockReceiver_hpp */ diff --git a/ClockReceiver/ClockingHintSource.hpp b/ClockReceiver/ClockingHintSource.hpp index e5f775e0d..be88ba404 100644 --- a/ClockReceiver/ClockingHintSource.hpp +++ b/ClockReceiver/ClockingHintSource.hpp @@ -6,8 +6,7 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef ClockingHintSource_hpp -#define ClockingHintSource_hpp +#pragma once namespace ClockingHint { @@ -84,5 +83,3 @@ class Source { }; } - -#endif /* ClockingHintSource_h */ diff --git a/ClockReceiver/DeferredQueue.hpp b/ClockReceiver/DeferredQueue.hpp index a2015aea3..5dfbfd0ea 100644 --- a/ClockReceiver/DeferredQueue.hpp +++ b/ClockReceiver/DeferredQueue.hpp @@ -6,8 +6,7 @@ // Copyright © 2018 Thomas Harte. All rights reserved. // -#ifndef DeferredQueue_h -#define DeferredQueue_h +#pragma once #include #include @@ -120,5 +119,3 @@ template class DeferredQueuePerformer: public DeferredQueue< private: std::function target_; }; - -#endif /* DeferredQueue_h */ diff --git a/ClockReceiver/DeferredValue.hpp b/ClockReceiver/DeferredValue.hpp index 35fbdf974..359186c67 100644 --- a/ClockReceiver/DeferredValue.hpp +++ b/ClockReceiver/DeferredValue.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef DeferredValue_h -#define DeferredValue_h +#pragma once /*! Provides storage for a single deferred value: one with a current value and a certain number @@ -44,5 +43,3 @@ template class DeferredValue { (backlog[DeferredDepth / elements_per_uint32] & insert_mask) | (value << insert_shift); } }; - -#endif /* DeferredValue_h */ diff --git a/ClockReceiver/ForceInline.hpp b/ClockReceiver/ForceInline.hpp index d3c3dc512..7610f2604 100644 --- a/ClockReceiver/ForceInline.hpp +++ b/ClockReceiver/ForceInline.hpp @@ -6,8 +6,7 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef ForceInline_hpp -#define ForceInline_hpp +#pragma once #ifndef NDEBUG @@ -22,5 +21,3 @@ #endif #endif - -#endif /* ForceInline_h */ diff --git a/ClockReceiver/JustInTime.hpp b/ClockReceiver/JustInTime.hpp index 040b26e4d..a8482f237 100644 --- a/ClockReceiver/JustInTime.hpp +++ b/ClockReceiver/JustInTime.hpp @@ -6,14 +6,15 @@ // Copyright © 2019 Thomas Harte. All rights reserved. // -#ifndef JustInTime_h -#define JustInTime_h +#pragma once #include "ClockReceiver.hpp" #include "../Concurrency/AsyncTaskQueue.hpp" #include "ClockingHintSource.hpp" #include "ForceInline.hpp" +#include + /*! A JustInTimeActor holds (i) an embedded object with a run_for method; and (ii) an amount of time since run_for was last called. @@ -24,7 +25,7 @@ Machines that accumulate HalfCycle time but supply to a Cycle-counted device may supply a separate @c TargetTimeScale at template declaration. - If the held object implements get_next_sequence_point() then it'll be used to flush implicitly + If the held object implements @c next_sequence_point() then it'll be used to flush implicitly as and when sequence points are hit. Callers can use will_flush() to predict these. If the held object is a subclass of ClockingHint::Source, this template will register as an @@ -38,7 +39,7 @@ template () { +#ifndef NDEBUG + assert(!flush_concurrency_check_.test_and_set()); +#endif flush(); +#ifndef NDEBUG + flush_concurrency_check_.clear(); +#endif return std::unique_ptr(&object_, SequencePointAwareDeleter(this)); } @@ -130,7 +137,13 @@ template () const { auto non_const_this = const_cast *>(this); +#ifndef NDEBUG + assert(!non_const_this->flush_concurrency_check_.test_and_set()); +#endif non_const_this->flush(); +#ifndef NDEBUG + non_const_this->flush_concurrency_check_.clear(); +#endif return std::unique_ptr(&object_, SequencePointAwareDeleter(non_const_this)); } @@ -233,9 +246,9 @@ template max translation rather than // allowing the arithmetic to overflow. if constexpr (divider == 1 && std::is_same_v) { - time_until_event_ = object_.get_next_sequence_point(); + time_until_event_ = object_.next_sequence_point(); } else { - const auto time = object_.get_next_sequence_point(); + const auto time = object_.next_sequence_point(); if(time == TargetTimeScale::max()) { time_until_event_ = LocalTimeScale::max(); } else { @@ -258,12 +271,16 @@ template struct has_sequence_points : std::false_type {}; - template struct has_sequence_points().get_next_sequence_point()))> : std::true_type {}; + template struct has_sequence_points().next_sequence_point()))> : std::true_type {}; ClockingHint::Preference clocking_preference_ = ClockingHint::Preference::JustInTime; void set_component_prefers_clocking(ClockingHint::Source *, ClockingHint::Preference clocking) { clocking_preference_ = clocking; } + +#ifndef NDEBUG + std::atomic_flag flush_concurrency_check_{}; +#endif }; /*! @@ -276,7 +293,7 @@ template AsyncJustInTimeActor(TargetTimeScale threshold, Args&&... args) : object_(std::forward(args)...), - threshold_(threshold) {} + threshold_(threshold) {} /// Adds time to the actor. inline void operator += (const LocalTimeScale &rhs) { @@ -317,5 +334,3 @@ template task_queue_; }; - -#endif /* JustInTime_h */ diff --git a/ClockReceiver/ScanSynchroniser.hpp b/ClockReceiver/ScanSynchroniser.hpp index be2682c44..c4f002d7b 100644 --- a/ClockReceiver/ScanSynchroniser.hpp +++ b/ClockReceiver/ScanSynchroniser.hpp @@ -6,8 +6,7 @@ // Copyright © 2020 Thomas Harte. All rights reserved. // -#ifndef ScanSynchroniser_h -#define ScanSynchroniser_h +#pragma once #include "../Outputs/ScanTarget.hpp" @@ -84,5 +83,3 @@ class ScanSynchroniser { }; } - -#endif /* ScanSynchroniser_h */ diff --git a/ClockReceiver/TimeTypes.hpp b/ClockReceiver/TimeTypes.hpp index c697b274b..118649b51 100644 --- a/ClockReceiver/TimeTypes.hpp +++ b/ClockReceiver/TimeTypes.hpp @@ -6,8 +6,7 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef TimeTypes_h -#define TimeTypes_h +#pragma once #include @@ -25,6 +24,3 @@ inline Seconds seconds(Nanos nanos) { } } - -#endif /* TimeTypes_h */ - diff --git a/ClockReceiver/VSyncPredictor.hpp b/ClockReceiver/VSyncPredictor.hpp index f34ed55c5..e77639a76 100644 --- a/ClockReceiver/VSyncPredictor.hpp +++ b/ClockReceiver/VSyncPredictor.hpp @@ -6,8 +6,7 @@ // Copyright © 2020 Thomas Harte. All rights reserved. // -#ifndef VSyncPredictor_hpp -#define VSyncPredictor_hpp +#pragma once #include "TimeTypes.hpp" #include @@ -151,5 +150,3 @@ class VSyncPredictor { }; } - -#endif /* VSyncPredictor_hpp */ diff --git a/Components/1770/1770.cpp b/Components/1770/1770.cpp index 28c9d5e72..cce894a12 100644 --- a/Components/1770/1770.cpp +++ b/Components/1770/1770.cpp @@ -9,10 +9,12 @@ #include "1770.hpp" #include "../../Storage/Disk/Encodings/MFM/Constants.hpp" - -#define LOG_PREFIX "[WD FDC] " #include "../../Outputs/Log.hpp" +namespace { +Log::Logger logger; +} + using namespace WD; WD1770::WD1770(Personality p) : @@ -29,10 +31,10 @@ void WD1770::write(int address, uint8_t value) { if((value&0xf0) == 0xd0) { if(value == 0xd0) { // Force interrupt **immediately**. - LOG("Force interrupt immediately"); + logger.info().append("Force interrupt immediately"); posit_event(int(Event1770::ForceInterrupt)); } else { - ERROR("!!!TODO: force interrupt!!!"); + logger.error().append("TODO: force interrupt"); update_status([] (Status &status) { status.type = Status::One; }); @@ -66,7 +68,7 @@ uint8_t WD1770::read(int address) { // Per Jean Louis-Guérin's documentation: // - // * the write-protect bit is locked into place by a type 2 or type 3 command, but is + // * the write-protect bit is locked into place by a type 2 or type 3 command, but is // read live after a type 1. // * the track 0 bit is captured during a type 1 instruction and lost upon any other type, // it is not live sampled. @@ -99,14 +101,14 @@ uint8_t WD1770::read(int address) { if(status_.type == Status::One) status |= (status_.spin_up ? Flag::SpinUp : 0); } -// LOG("Returned status " << PADHEX(2) << int(status) << " of type " << 1+int(status_.type)); +// logger.info().append("Returned status %02x of type %d", status, 1+int(status_.type)); return status; } case 1: - LOG("Returned track " << int(track_)); + logger.info().append("Returned track %d", track_); return track_; case 2: - LOG("Returned sector " << int(sector_)); + logger.info().append("Returned sector %d", sector_); return sector_; case 3: update_status([] (Status &status) { @@ -212,7 +214,7 @@ void WD1770::posit_event(int new_event_type) { // Wait for a new command, branch to the appropriate handler. case 0: wait_for_command: - LOG("Idle..."); + logger.info().append("Idle..."); set_data_mode(DataMode::Scanning); index_hole_count_ = 0; @@ -229,7 +231,7 @@ void WD1770::posit_event(int new_event_type) { status.track_zero = false; // Always reset by a non-type 1; so reset regardless and set properly later. }); - LOG("Starting " << PADHEX(2) << int(command_)); + logger.info().append("Starting %02x", command_); if(!(command_ & 0x80)) goto begin_type_1; if(!(command_ & 0x40)) goto begin_type_2; @@ -259,7 +261,7 @@ void WD1770::posit_event(int new_event_type) { status.data_request = false; }); - LOG("Step/Seek/Restore with track " << int(track_) << " data " << int(data_)); + logger.info().append("Step/Seek/Restore with track %d data %d", track_, data_); if(!has_motor_on_line() && !has_head_load_line()) goto test_type1_type; if(has_motor_on_line()) goto begin_type1_spin_up; @@ -339,7 +341,7 @@ void WD1770::posit_event(int new_event_type) { READ_ID(); if(index_hole_count_ == 6) { - LOG("Nothing found to verify"); + logger.info().append("Nothing found to verify"); update_status([] (Status &status) { status.seek_error = true; }); @@ -357,7 +359,7 @@ void WD1770::posit_event(int new_event_type) { } if(header_[0] == track_) { - LOG("Reached track " << std::dec << int(track_)); + logger.info().append("Reached track %d", track_); update_status([] (Status &status) { status.crc_error = false; }); @@ -430,7 +432,7 @@ void WD1770::posit_event(int new_event_type) { READ_ID(); if(index_hole_count_ == 5) { - LOG("Failed to find sector " << std::dec << int(sector_)); + logger.info().append("Failed to find sector %d", sector_); update_status([] (Status &status) { status.record_not_found = true; }); @@ -440,12 +442,12 @@ void WD1770::posit_event(int new_event_type) { distance_into_section_ = 0; set_data_mode(DataMode::Scanning); - LOG("Considering " << std::dec << int(header_[0]) << "/" << int(header_[2])); + logger.info().append("Considering %d/%d", header_[0], header_[2]); if( header_[0] == track_ && header_[2] == sector_ && (has_motor_on_line() || !(command_&0x02) || ((command_&0x08) >> 3) == header_[1])) { - LOG("Found " << std::dec << int(header_[0]) << "/" << int(header_[2])); + logger.info().append("Found %d/%d", header_[0], header_[2]); if(get_crc_generator().get_value()) { - LOG("CRC error; back to searching"); + logger.info().append("CRC error; back to searching"); update_status([] (Status &status) { status.crc_error = true; }); @@ -503,18 +505,18 @@ void WD1770::posit_event(int new_event_type) { set_data_mode(DataMode::Scanning); if(get_crc_generator().get_value()) { - LOG("CRC error; terminating"); + logger.info().append("CRC error; terminating"); update_status([] (Status &status) { status.crc_error = true; }); goto wait_for_command; } - LOG("Finished reading sector " << std::dec << int(sector_)); + logger.info().append("Finished reading sector %d", sector_); if(command_ & 0x10) { sector_++; - LOG("Advancing to search for sector " << std::dec << int(sector_)); + logger.info().append("Advancing to search for sector %d", sector_); goto test_type2_write_protection; } goto wait_for_command; @@ -598,7 +600,7 @@ void WD1770::posit_event(int new_event_type) { sector_++; goto test_type2_write_protection; } - LOG("Wrote sector " << std::dec << int(sector_)); + logger.info().append("Wrote sector %d", sector_); goto wait_for_command; diff --git a/Components/1770/1770.hpp b/Components/1770/1770.hpp index 0c25edeb2..6a6833f04 100644 --- a/Components/1770/1770.hpp +++ b/Components/1770/1770.hpp @@ -6,8 +6,7 @@ // Copyright 2016 Thomas Harte. All rights reserved. // -#ifndef _770_hpp -#define _770_hpp +#pragma once #include "../../Storage/Disk/Controller/MFMDiskController.hpp" @@ -141,5 +140,3 @@ class WD1770: public Storage::Disk::MFMController { }; } - -#endif /* _770_hpp */ diff --git a/Components/5380/ncr5380.cpp b/Components/5380/ncr5380.cpp index 1d89d4eac..564776cde 100644 --- a/Components/5380/ncr5380.cpp +++ b/Components/5380/ncr5380.cpp @@ -8,13 +8,11 @@ #include "ncr5380.hpp" -#ifndef NDEBUG -#define NDEBUG -#endif -#define LOG_PREFIX "[5380] " - #include "../../Outputs/Log.hpp" +namespace { +Log::Logger logger; +} // TODO: // // end_of_dma_ should be set if: /EOP && /DACK && (/RD || /WR); for at least 100ns. @@ -38,7 +36,7 @@ NCR5380::NCR5380(SCSI::Bus &bus, int clock_rate) : void NCR5380::write(int address, uint8_t value, bool) { switch(address & 7) { case 0: - LOG("[0] Set current SCSI bus state to " << PADHEX(2) << int(value)); + logger.info().append("[0] Set current SCSI bus state to %02x", value); data_bus_ = value; if(dma_request_ && dma_operation_ == DMAOperation::Send) { @@ -47,7 +45,7 @@ void NCR5380::write(int address, uint8_t value, bool) { break; case 1: { - LOG("[1] Initiator command register set: " << PADHEX(2) << int(value)); + logger.info().append("[1] Initiator command register set: %02x", value); initiator_command_ = value; bus_output_ &= ~(Line::Reset | Line::Acknowledge | Line::Busy | Line::SelectTarget | Line::Attention); @@ -63,7 +61,7 @@ void NCR5380::write(int address, uint8_t value, bool) { } break; case 2: - LOG("[2] Set mode: " << PADHEX(2) << int(value)); + logger.info().append("[2] Set mode: %02x", value); mode_ = value; // bit 7: 1 = use block mode DMA mode (if DMA mode is also enabled) @@ -104,27 +102,27 @@ void NCR5380::write(int address, uint8_t value, bool) { break; case 3: { - LOG("[3] Set target command: " << PADHEX(2) << int(value)); + logger.info().append("[3] Set target command: %02x", value); target_command_ = value; update_control_output(); } break; case 4: - LOG("[4] Set select enabled: " << PADHEX(2) << int(value)); + logger.info().append("[4] Set select enabled: %02x", value); break; case 5: - LOG("[5] Start DMA send: " << PADHEX(2) << int(value)); + logger.info().append("[5] Start DMA send: %02x", value); dma_operation_ = DMAOperation::Send; break; case 6: - LOG("[6] Start DMA target receive: " << PADHEX(2) << int(value)); + logger.info().append("[6] Start DMA target receive: %02x", value); dma_operation_ = DMAOperation::TargetReceive; break; case 7: - LOG("[7] Start DMA initiator receive: " << PADHEX(2) << int(value)); + logger.info().append("[7] Start DMA initiator receive: %02x", value); dma_operation_ = DMAOperation::InitiatorReceive; break; } @@ -148,7 +146,7 @@ void NCR5380::write(int address, uint8_t value, bool) { uint8_t NCR5380::read(int address, bool) { switch(address & 7) { case 0: - LOG("[0] Get current SCSI bus state: " << PADHEX(2) << (bus_.get_state() & 0xff)); + logger.info().append("[0] Get current SCSI bus state: %02x", (bus_.get_state() & 0xff)); if(dma_request_ && dma_operation_ == DMAOperation::InitiatorReceive) { return dma_acknowledge(); @@ -156,7 +154,7 @@ uint8_t NCR5380::read(int address, bool) { return uint8_t(bus_.get_state()); case 1: - LOG("[1] Initiator command register get: " << (arbitration_in_progress_ ? 'p' : '-') << (lost_arbitration_ ? 'l' : '-')); + logger.info().append("[1] Initiator command register get: %c%c", arbitration_in_progress_ ? 'p' : '-', lost_arbitration_ ? 'l' : '-'); return // Bits repeated as they were set. (initiator_command_ & ~0x60) | @@ -168,11 +166,11 @@ uint8_t NCR5380::read(int address, bool) { (lost_arbitration_ ? 0x20 : 0x00); case 2: - LOG("[2] Get mode"); + logger.info().append("[2] Get mode"); return mode_; case 3: - LOG("[3] Get target command"); + logger.info().append("[3] Get target command"); return target_command_; case 4: { @@ -186,7 +184,7 @@ uint8_t NCR5380::read(int address, bool) { ((bus_state & Line::Input) ? 0x04 : 0x00) | ((bus_state & Line::SelectTarget) ? 0x02 : 0x00) | ((bus_state & Line::Parity) ? 0x01 : 0x00); - LOG("[4] Get current bus state: " << PADHEX(2) << int(result)); + logger.info().append("[4] Get current bus state: %02x", result); return result; } @@ -201,16 +199,16 @@ uint8_t NCR5380::read(int address, bool) { /* b2 = busy error */ ((bus_state & Line::Attention) ? 0x02 : 0x00) | ((bus_state & Line::Acknowledge) ? 0x01 : 0x00); - LOG("[5] Get bus and status: " << PADHEX(2) << int(result)); + logger.info().append("[5] Get bus and status: %02x", result); return result; } case 6: - LOG("[6] Get input data"); + logger.info().append("[6] Get input data"); return 0xff; case 7: - LOG("[7] Reset parity/interrupt"); + logger.info().append("[7] Reset parity/interrupt"); irq_ = false; return 0xff; } diff --git a/Components/5380/ncr5380.hpp b/Components/5380/ncr5380.hpp index 6ea409b53..48a7d3e0a 100644 --- a/Components/5380/ncr5380.hpp +++ b/Components/5380/ncr5380.hpp @@ -6,16 +6,14 @@ // Copyright © 2019 Thomas Harte. All rights reserved. // -#ifndef ncr5380_hpp -#define ncr5380_hpp +#pragma once #include #include "../../Storage/MassStorage/SCSI/SCSI.hpp" -namespace NCR { -namespace NCR5380 { +namespace NCR::NCR5380 { /*! Models the NCR 5380, a SCSI interface chip. @@ -24,7 +22,7 @@ class NCR5380 final: public SCSI::Bus::Observer { public: NCR5380(SCSI::Bus &bus, int clock_rate); - /*! Writes @c value to @c address. */ + /*! Writes @c value to @c address. */ void write(int address, uint8_t value, bool dma_acknowledge = false); /*! Reads from @c address. */ @@ -87,6 +85,3 @@ class NCR5380 final: public SCSI::Bus::Observer { }; } -} - -#endif /* ncr5380_hpp */ diff --git a/Components/6522/6522.hpp b/Components/6522/6522.hpp index 3e09819d0..df8502f71 100644 --- a/Components/6522/6522.hpp +++ b/Components/6522/6522.hpp @@ -6,8 +6,7 @@ // Copyright 2016 Thomas Harte. All rights reserved. // -#ifndef _522_hpp -#define _522_hpp +#pragma once #include @@ -15,8 +14,7 @@ #include "../../ClockReceiver/ClockReceiver.hpp" -namespace MOS { -namespace MOS6522 { +namespace MOS::MOS6522 { enum Port { A = 0, @@ -138,9 +136,6 @@ template class MOS6522: public MOS6522Storage { void evaluate_port_b_output(); }; -} } #include "Implementation/6522Implementation.hpp" - -#endif /* _522_hpp */ diff --git a/Components/6522/Implementation/6522Implementation.hpp b/Components/6522/Implementation/6522Implementation.hpp index 3e23422eb..29c4aed12 100644 --- a/Components/6522/Implementation/6522Implementation.hpp +++ b/Components/6522/Implementation/6522Implementation.hpp @@ -12,8 +12,7 @@ // // PB6 count-down mode for timer 2. -namespace MOS { -namespace MOS6522 { +namespace MOS::MOS6522 { template void MOS6522::access(int address) { switch(address) { @@ -272,7 +271,7 @@ template void MOS6522::set_control_line_input(Port port, Line li // TODO: and at least one full clock since the shift register was written? if(port == Port::B) { switch(shift_mode()) { - default: break; + default: break; case ShiftMode::InUnderCB1: if(value) shift_in(); break; // Shifts in are captured on a low-to-high transition. case ShiftMode::OutUnderCB1: if(!value) shift_out(); break; // Shifts out are updated on a high-to-low transition. } @@ -330,7 +329,7 @@ template void MOS6522::do_phase2() { // If the shift register is shifting according to the input clock, do a shift. switch(shift_mode()) { - default: break; + default: break; case ShiftMode::InUnderPhase2: shift_in(); break; case ShiftMode::OutUnderPhase2: shift_out(); break; } @@ -346,9 +345,9 @@ template void MOS6522::do_phase1() { // If the shift register is shifting according to this timer, do a shift. // TODO: "shift register is driven by only the low order 8 bits of timer 2"? switch(shift_mode()) { - default: break; + default: break; case ShiftMode::InUnderT2: shift_in(); break; - case ShiftMode::OutUnderT2FreeRunning: shift_out(); break; + case ShiftMode::OutUnderT2FreeRunning: shift_out(); break; case ShiftMode::OutUnderT2: shift_out(); break; // TODO: present a clock on CB1. } @@ -494,4 +493,3 @@ template void MOS6522::shift_out() { } } -} diff --git a/Components/6522/Implementation/6522Storage.hpp b/Components/6522/Implementation/6522Storage.hpp index 0c7f4c789..330081e9a 100644 --- a/Components/6522/Implementation/6522Storage.hpp +++ b/Components/6522/Implementation/6522Storage.hpp @@ -6,13 +6,11 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef _522Storage_hpp -#define _522Storage_hpp +#pragma once #include -namespace MOS { -namespace MOS6522 { +namespace MOS::MOS6522 { class MOS6522Storage { protected: @@ -108,6 +106,3 @@ class MOS6522Storage { }; } -} - -#endif /* _522Storage_hpp */ diff --git a/Components/6526/6526.hpp b/Components/6526/6526.hpp index 99eb5e8e6..508994e46 100644 --- a/Components/6526/6526.hpp +++ b/Components/6526/6526.hpp @@ -6,16 +6,14 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef _526_h -#define _526_h +#pragma once #include #include "Implementation/6526Storage.hpp" #include "../Serial/Line.hpp" -namespace MOS { -namespace MOS6526 { +namespace MOS::MOS6526 { enum Port { A = 0, @@ -86,9 +84,6 @@ template class MOS6526: bool serial_line_did_produce_bit(Serial::Line *line, int bit) final; }; -} } #include "Implementation/6526Implementation.hpp" - -#endif /* _526_h */ diff --git a/Components/6526/Implementation/6526Implementation.hpp b/Components/6526/Implementation/6526Implementation.hpp index 92ecf5269..28889cb7f 100644 --- a/Components/6526/Implementation/6526Implementation.hpp +++ b/Components/6526/Implementation/6526Implementation.hpp @@ -6,14 +6,12 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef _526Implementation_h -#define _526Implementation_h +#pragma once #include #include -namespace MOS { -namespace MOS6526 { +namespace MOS::MOS6526 { enum Interrupts: uint8_t { TimerA = 1 << 0, @@ -239,6 +237,3 @@ bool MOS6526::serial_line_did_produce_bit(Serial::Line } } -} - -#endif /* _526Implementation_h */ diff --git a/Components/6526/Implementation/6526Storage.hpp b/Components/6526/Implementation/6526Storage.hpp index 7a3c2bf1b..0c3695eab 100644 --- a/Components/6526/Implementation/6526Storage.hpp +++ b/Components/6526/Implementation/6526Storage.hpp @@ -6,15 +6,13 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef _526Storage_h -#define _526Storage_h +#pragma once #include #include "../../../ClockReceiver/ClockReceiver.hpp" -namespace MOS { -namespace MOS6526 { +namespace MOS::MOS6526 { class TODBase { public: @@ -323,8 +321,6 @@ struct MOS6526Storage { static constexpr int TestInputNow = 1 << 8; static constexpr int PendingClearMask = ~(ReloadNow | OneShotNow | ApplyClockNow); - - bool active_ = false; } counter_[2]; static constexpr int InterruptInOne = 1 << 0; @@ -334,6 +330,3 @@ struct MOS6526Storage { }; } -} - -#endif /* _526Storage_h */ diff --git a/Components/6532/6532.hpp b/Components/6532/6532.hpp index 4607e1a8e..23a86769b 100644 --- a/Components/6532/6532.hpp +++ b/Components/6532/6532.hpp @@ -6,8 +6,7 @@ // Copyright 2016 Thomas Harte. All rights reserved. // -#ifndef _532_hpp -#define _532_hpp +#pragma once #include #include @@ -188,5 +187,3 @@ template class MOS6532 { }; } - -#endif /* _532_hpp */ diff --git a/Components/6560/6560.hpp b/Components/6560/6560.hpp index 1ebd3e6e8..f8c570924 100644 --- a/Components/6560/6560.hpp +++ b/Components/6560/6560.hpp @@ -6,8 +6,7 @@ // Copyright 2016 Thomas Harte. All rights reserved. // -#ifndef _560_hpp -#define _560_hpp +#pragma once #include "../../ClockReceiver/ClockReceiver.hpp" #include "../../Concurrency/AsyncTaskQueue.hpp" @@ -15,8 +14,7 @@ #include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" #include "../../Outputs/Speaker/Implementation/SampleSource.hpp" -namespace MOS { -namespace MOS6560 { +namespace MOS::MOS6560 { // audio state class AudioGenerator: public ::Outputs::Speaker::SampleSource { @@ -84,11 +82,11 @@ template class MOS6560 { speaker_.set_input_rate(float(clock_rate / 4.0)); } - void set_scan_target(Outputs::Display::ScanTarget *scan_target) { crt_.set_scan_target(scan_target); } + void set_scan_target(Outputs::Display::ScanTarget *scan_target) { crt_.set_scan_target(scan_target); } Outputs::Display::ScanStatus get_scaled_scan_status() const { return crt_.get_scaled_scan_status() / 4.0f; } - void set_display_type(Outputs::Display::DisplayType display_type) { crt_.set_display_type(display_type); } - Outputs::Display::DisplayType get_display_type() const { return crt_.get_display_type(); } - Outputs::Speaker::Speaker *get_speaker() { return &speaker_; } + void set_display_type(Outputs::Display::DisplayType display_type) { crt_.set_display_type(display_type); } + Outputs::Display::DisplayType get_display_type() const { return crt_.get_display_type(); } + Outputs::Speaker::Speaker *get_speaker() { return &speaker_; } void set_high_frequency_cutoff(float cutoff) { speaker_.set_high_frequency_cutoff(cutoff); @@ -279,7 +277,7 @@ template class MOS6560 { switch(output_state_) { case State::Sync: crt_.output_sync(cycles_in_state_ * 4); break; case State::ColourBurst: crt_.output_colour_burst(cycles_in_state_ * 4, (is_odd_frame_ || is_odd_line_) ? 128 : 0); break; - case State::Border: output_border(cycles_in_state_ * 4); break; + case State::Border: crt_.output_level(cycles_in_state_ * 4, registers_.borderColour); break; case State::Pixels: crt_.output_data(cycles_in_state_ * 4); break; } output_state_ = this_state_; @@ -401,7 +399,7 @@ template class MOS6560 { case 0xf: { uint16_t new_border_colour = colours_[value & 0x07]; if(this_state_ == State::Border && new_border_colour != registers_.borderColour) { - output_border(cycles_in_state_ * 4); + crt_.output_level(cycles_in_state_ * 4, registers_.borderColour); cycles_in_state_ = 0; } registers_.invertedCells = !((value >> 3)&1); @@ -504,11 +502,6 @@ template class MOS6560 { uint16_t colours_[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; uint16_t *pixel_pointer = nullptr; - void output_border(int number_of_cycles) { - uint16_t *colour_pointer = reinterpret_cast(crt_.begin_data(1)); - if(colour_pointer) *colour_pointer = registers_.borderColour; - crt_.output_level(number_of_cycles); - } struct { int cycles_per_line = 0; @@ -521,6 +514,3 @@ template class MOS6560 { }; } -} - -#endif /* _560_hpp */ diff --git a/Components/6845/CRTC6845.hpp b/Components/6845/CRTC6845.hpp index 7006a37e9..8f910a696 100644 --- a/Components/6845/CRTC6845.hpp +++ b/Components/6845/CRTC6845.hpp @@ -6,16 +6,14 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef CRTC6845_hpp -#define CRTC6845_hpp +#pragma once #include "../../ClockReceiver/ClockReceiver.hpp" #include #include -namespace Motorola { -namespace CRTC { +namespace Motorola::CRTC { struct BusState { bool display_enable = false; @@ -24,6 +22,10 @@ struct BusState { bool cursor = false; uint16_t refresh_address = 0; uint16_t row_address = 0; + + // Not strictly part of the bus state; provided because the partition between 6845 and bus handler + // doesn't quite hold up in some emulated systems where the two are integrated and share more state. + int field_count = 0; }; class BusHandler { @@ -43,31 +45,49 @@ class BusHandler { void perform_bus_cycle_phase2(const BusState &) {} }; -enum Personality { +enum class Personality { HD6845S, // Type 0 in CPC parlance. Zero-width HSYNC available, no status, programmable VSYNC length. // Considered exactly identical to the UM6845, so this enum covers both. UM6845R, // Type 1 in CPC parlance. Status register, fixed-length VSYNC. MC6845, // Type 2. No status register, fixed-length VSYNC, no zero-length HSYNC. - AMS40226 // Type 3. Status is get register, fixed-length VSYNC, no zero-length HSYNC. + AMS40226, // Type 3. Status is get register, fixed-length VSYNC, no zero-length HSYNC. + + EGA, // Extended EGA-style CRTC; uses 16-bit addressing throughout. +}; + +constexpr bool is_egavga(Personality p) { + return p >= Personality::EGA; +} + +// https://www.pcjs.org/blog/2018/03/20/ advises that "the behavior of bits 5 and 6 [of register 10, the cursor start +// register is really card specific". +// +// This enum captures those specifics. +enum class CursorType { + /// No cursor signal is generated. + None, + /// MDA style: 00 => symmetric blinking; 01 or 10 => no blinking; 11 => short on, long off. + MDA, + /// EGA style: ignore the bits completely. + EGA, }; // TODO UM6845R and R12/R13; see http://www.cpcwiki.eu/index.php/CRTC#CRTC_Differences -template class CRTC6845 { +template class CRTC6845 { public: - - CRTC6845(Personality p, T &bus_handler) noexcept : - personality_(p), bus_handler_(bus_handler), status_(0) {} + CRTC6845(BusHandlerT &bus_handler) noexcept : + bus_handler_(bus_handler), status_(0) {} void select_register(uint8_t r) { selected_register_ = r; } uint8_t get_status() { - switch(personality_) { - case UM6845R: return status_ | (bus_state_.vsync ? 0x20 : 0x00); - case AMS40226: return get_register(); - default: return 0xff; + switch(personality) { + case Personality::UM6845R: return status_ | (bus_state_.vsync ? 0x20 : 0x00); + case Personality::AMS40226: return get_register(); + default: return 0xff; } return 0xff; } @@ -76,30 +96,85 @@ template class CRTC6845 { if(selected_register_ == 31) status_ &= ~0x80; if(selected_register_ == 16 || selected_register_ == 17) status_ &= ~0x40; - if(personality_ == UM6845R && selected_register_ == 31) return dummy_register_; + if(personality == Personality::UM6845R && selected_register_ == 31) return dummy_register_; if(selected_register_ < 12 || selected_register_ > 17) return 0xff; return registers_[selected_register_]; } void set_register(uint8_t value) { - static uint8_t masks[] = { - 0xff, 0xff, 0xff, 0xff, 0x7f, 0x1f, 0x7f, 0x7f, - 0xff, 0x1f, 0x7f, 0x1f, 0x3f, 0xff, 0x3f, 0xff + static constexpr bool is_ega = is_egavga(personality); + + auto load_low = [value](uint16_t &target) { + target = (target & 0xff00) | value; + }; + auto load_high = [value](uint16_t &target) { + constexpr uint8_t mask = RefreshMask >> 8; + target = uint16_t((target & 0x00ff) | ((value & mask) << 8)); }; - // Per CPC documentation, skew doesn't work on a "type 1 or 2", i.e. an MC6845 or a UM6845R. - if(selected_register_ == 8 && personality_ != UM6845R && personality_ != MC6845) { - switch((value >> 4)&3) { - default: display_skew_mask_ = 1; break; - case 1: display_skew_mask_ = 2; break; - case 2: display_skew_mask_ = 4; break; - } + switch(selected_register_) { + case 0: layout_.horizontal.total = value; break; + case 1: layout_.horizontal.displayed = value; break; + case 2: layout_.horizontal.start_sync = value; break; + case 3: + layout_.horizontal.sync_width = value & 0xf; + layout_.vertical.sync_lines = value >> 4; + // TODO: vertical sync lines: + // "(0 means 16 on some CRTC. Not present on all CRTCs, fixed to 16 lines on these)" + break; + case 4: layout_.vertical.total = value & 0x7f; break; + case 5: layout_.vertical.adjust = value & 0x1f; break; + case 6: layout_.vertical.displayed = value & 0x7f; break; + case 7: layout_.vertical.start_sync = value & 0x7f; break; + case 8: + switch(value & 3) { + default: layout_.interlace_mode_ = InterlaceMode::Off; break; + case 0b01: layout_.interlace_mode_ = InterlaceMode::InterlaceSync; break; + case 0b11: layout_.interlace_mode_ = InterlaceMode::InterlaceSyncAndVideo; break; + } + + // Per CPC documentation, skew doesn't work on a "type 1 or 2", i.e. an MC6845 or a UM6845R. + if(personality != Personality::UM6845R && personality != Personality::MC6845) { + switch((value >> 4)&3) { + default: display_skew_mask_ = 1; break; + case 1: display_skew_mask_ = 2; break; + case 2: display_skew_mask_ = 4; break; + } + } + break; + case 9: layout_.vertical.end_row = value & 0x1f; break; + case 10: + layout_.vertical.start_cursor = value & 0x1f; + layout_.cursor_flags = (value >> 5) & 3; + break; + case 11: + layout_.vertical.end_cursor = value & 0x1f; + break; + case 12: load_high(layout_.start_address); break; + case 13: load_low(layout_.start_address); break; + case 14: load_high(layout_.cursor_address); break; + case 15: load_low(layout_.cursor_address); break; } + static constexpr uint8_t masks[] = { + 0xff, // Horizontal total. + 0xff, // Horizontal display end. + 0xff, // Start horizontal blank. + 0xff, // + // EGA: b0–b4: end of horizontal blank; + // b5–b6: "Number of character clocks to delay start of display after Horizontal Total has been reached." + + is_ega ? 0xff : 0x7f, // Start horizontal retrace. + 0x1f, 0x7f, 0x7f, + 0xff, 0x1f, 0x7f, 0x1f, + uint8_t(RefreshMask >> 8), uint8_t(RefreshMask), + uint8_t(RefreshMask >> 8), uint8_t(RefreshMask), + }; + if(selected_register_ < 16) { registers_[selected_register_] = value & masks[selected_register_]; } - if(selected_register_ == 31 && personality_ == UM6845R) { + if(selected_register_ == 31 && personality == Personality::UM6845R) { dummy_register_ = value; } } @@ -113,45 +188,48 @@ template class CRTC6845 { void run_for(Cycles cycles) { auto cyles_remaining = cycles.as_integral(); while(cyles_remaining--) { - // check for end of visible characters - if(character_counter_ == registers_[1]) { + // Check for end of visible characters. + if(character_counter_ == layout_.horizontal.displayed) { // TODO: consider skew in character_is_visible_. Or maybe defer until perform_bus_cycle? character_is_visible_ = false; end_of_line_address_ = bus_state_.refresh_address; } perform_bus_cycle_phase1(); - bus_state_.refresh_address = (bus_state_.refresh_address + 1) & 0x3fff; + bus_state_.refresh_address = (bus_state_.refresh_address + 1) & RefreshMask; - // check for end-of-line - if(character_counter_ == registers_[0]) { + bus_state_.cursor = is_cursor_line_ && + bus_state_.refresh_address == layout_.cursor_address; + + // Check for end-of-line. + if(character_counter_ == layout_.horizontal.total) { character_counter_ = 0; do_end_of_line(); character_is_visible_ = true; } else { - // increment counter + // Increment counter. character_counter_++; } - // check for start of horizontal sync - if(character_counter_ == registers_[2]) { + // Check for start of horizontal sync. + if(character_counter_ == layout_.horizontal.start_sync) { hsync_counter_ = 0; bus_state_.hsync = true; } - // check for end of horizontal sync; note that a sync time of zero will result in an immediate + // Check for end of horizontal sync; note that a sync time of zero will result in an immediate // cancellation of the plan to perform sync if this is an HD6845S or UM6845R; otherwise zero // will end up counting as 16 as it won't be checked until after overflow. if(bus_state_.hsync) { - switch(personality_) { - case HD6845S: - case UM6845R: - bus_state_.hsync = hsync_counter_ != (registers_[3] & 15); + switch(personality) { + case Personality::HD6845S: + case Personality::UM6845R: + bus_state_.hsync = hsync_counter_ != layout_.horizontal.sync_width; hsync_counter_ = (hsync_counter_ + 1) & 15; break; default: hsync_counter_ = (hsync_counter_ + 1) & 15; - bus_state_.hsync = hsync_counter_ != (registers_[3] & 15); + bus_state_.hsync = hsync_counter_ != layout_.horizontal.sync_width; break; } } @@ -165,6 +243,8 @@ template class CRTC6845 { } private: + static constexpr uint16_t RefreshMask = (personality >= Personality::EGA) ? 0xffff : 0x3fff; + inline void perform_bus_cycle_phase1() { // Skew theory of operation: keep a history of the last three states, and apply whichever is selected. character_is_visible_shifter_ = (character_is_visible_shifter_ << 1) | unsigned(character_is_visible_); @@ -177,15 +257,21 @@ template class CRTC6845 { } inline void do_end_of_line() { - // check for end of vertical sync + if constexpr (cursor_type != CursorType::None) { + // Check for cursor disable. + // TODO: this is handled differently on the EGA, should I ever implement that. + is_cursor_line_ &= bus_state_.row_address != layout_.vertical.end_cursor; + } + + // Check for end of vertical sync. if(bus_state_.vsync) { vsync_counter_ = (vsync_counter_ + 1) & 15; - // on the UM6845R and AMS40226, honour the programmed vertical sync time; on the other CRTCs + // On the UM6845R and AMS40226, honour the programmed vertical sync time; on the other CRTCs // always use a vertical sync count of 16. - switch(personality_) { - case HD6845S: - case AMS40226: - bus_state_.vsync = vsync_counter_ != (registers_[3] >> 4); + switch(personality) { + case Personality::HD6845S: + case Personality::AMS40226: + bus_state_.vsync = vsync_counter_ != layout_.vertical.sync_lines; break; default: bus_state_.vsync = vsync_counter_ != 0; @@ -195,19 +281,19 @@ template class CRTC6845 { if(is_in_adjustment_period_) { line_counter_++; - if(line_counter_ == registers_[5]) { + if(line_counter_ == layout_.vertical.adjust) { is_in_adjustment_period_ = false; do_end_of_frame(); } } else { - // advance vertical counter - if(bus_state_.row_address == registers_[9]) { + // Advance vertical counter. + if(bus_state_.row_address == layout_.vertical.end_row) { bus_state_.row_address = 0; line_address_ = end_of_line_address_; - // check for entry into the overflow area - if(line_counter_ == registers_[4]) { - if(registers_[5]) { + // Check for entry into the overflow area. + if(line_counter_ == layout_.vertical.total) { + if(layout_.vertical.adjust) { line_counter_ = 0; is_in_adjustment_period_ = true; } else { @@ -216,14 +302,14 @@ template class CRTC6845 { } else { line_counter_ = (line_counter_ + 1) & 0x7f; - // check for start of vertical sync - if(line_counter_ == registers_[7]) { + // Check for start of vertical sync. + if(line_counter_ == layout_.vertical.start_sync) { bus_state_.vsync = true; vsync_counter_ = 0; } - // check for end of visible lines - if(line_counter_ == registers_[6]) { + // Check for end of visible lines. + if(line_counter_ == layout_.vertical.displayed) { line_is_visible_ = false; } } @@ -234,21 +320,76 @@ template class CRTC6845 { bus_state_.refresh_address = line_address_; character_counter_ = 0; - character_is_visible_ = (registers_[1] != 0); + character_is_visible_ = (layout_.horizontal.displayed != 0); + + if constexpr (cursor_type != CursorType::None) { + // Check for cursor enable. + is_cursor_line_ |= bus_state_.row_address == layout_.vertical.start_cursor; + + switch(cursor_type) { + // MDA-style blinking. + // https://retrocomputing.stackexchange.com/questions/27803/what-are-the-blinking-rates-of-the-caret-and-of-blinking-text-on-pc-graphics-car + // gives an 8/8 pattern for regular blinking though mode 11 is then just a guess. + case CursorType::MDA: + switch(layout_.cursor_flags) { + case 0b11: is_cursor_line_ &= (bus_state_.field_count & 8) < 3; break; + case 0b00: is_cursor_line_ &= bool(bus_state_.field_count & 8); break; + case 0b01: is_cursor_line_ = false; break; + case 0b10: is_cursor_line_ = true; break; + default: break; + } + break; + } + } } inline void do_end_of_frame() { line_counter_ = 0; line_is_visible_ = true; - line_address_ = uint16_t((registers_[12] << 8) | registers_[13]); + line_address_ = layout_.start_address; bus_state_.refresh_address = line_address_; + ++bus_state_.field_count; } - Personality personality_; - T &bus_handler_; + BusHandlerT &bus_handler_; BusState bus_state_; - uint8_t registers_[18] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + enum class InterlaceMode { + Off, + InterlaceSync, + InterlaceSyncAndVideo, + }; + enum class BlinkMode { + // TODO. + }; + struct { + struct { + uint8_t total; + uint8_t displayed; + uint8_t start_sync; + uint8_t sync_width; + } horizontal; + + struct { + uint8_t total; + uint8_t displayed; + uint8_t start_sync; + uint8_t sync_lines; + uint8_t adjust; + + uint8_t end_row; + uint8_t start_cursor; + uint8_t end_cursor; + } vertical; + + InterlaceMode interlace_mode_ = InterlaceMode::Off; + uint16_t start_address; + uint16_t cursor_address; + uint16_t light_pen_address; + uint8_t cursor_flags; + } layout_; + + uint8_t registers_[18]{}; uint8_t dummy_register_ = 0; int selected_register_ = 0; @@ -267,9 +408,8 @@ template class CRTC6845 { int display_skew_mask_ = 1; unsigned int character_is_visible_shifter_ = 0; + + bool is_cursor_line_ = false; }; } -} - -#endif /* CRTC6845_hpp */ diff --git a/Components/6850/6850.hpp b/Components/6850/6850.hpp index 69e192e90..88fe92996 100644 --- a/Components/6850/6850.hpp +++ b/Components/6850/6850.hpp @@ -6,8 +6,7 @@ // Copyright © 2019 Thomas Harte. All rights reserved. // -#ifndef Motorola_ACIA_6850_hpp -#define Motorola_ACIA_6850_hpp +#pragma once #include #include "../../ClockReceiver/ClockReceiver.hpp" @@ -15,8 +14,7 @@ #include "../../ClockReceiver/ClockingHintSource.hpp" #include "../Serial/Line.hpp" -namespace Motorola { -namespace ACIA { +namespace Motorola::ACIA { class ACIA: public ClockingHint::Source, private Serial::Line::ReadDelegate { public: @@ -127,6 +125,3 @@ class ACIA: public ClockingHint::Source, private Serial::Line::ReadDelega }; } -} - -#endif /* Motorola_ACIA_6850_hpp */ diff --git a/Components/68901/MFP68901.cpp b/Components/68901/MFP68901.cpp index c2f04d631..02ef79588 100644 --- a/Components/68901/MFP68901.cpp +++ b/Components/68901/MFP68901.cpp @@ -11,13 +11,14 @@ #include #include -#ifndef NDEBUG -#define NDEBUG -#endif - -#define LOG_PREFIX "[MFP] " #include "../../Outputs/Log.hpp" +namespace { + +Log::Logger logger; + +} + using namespace Motorola::MFP68901; ClockingHint::Preference MFP68901::preferred_clocking() const { @@ -64,11 +65,11 @@ uint8_t MFP68901::read(int address) { case 0x11: case 0x12: return get_timer_data(address - 0xf); // USART block: TODO. - case 0x13: LOG("Read: sync character generator"); break; - case 0x14: LOG("Read: USART control"); break; - case 0x15: LOG("Read: receiver status"); break; - case 0x16: LOG("Read: transmitter status"); break; - case 0x17: LOG("Read: USART data"); break; + case 0x13: logger.error().append("Read: sync character generator"); break; + case 0x14: logger.error().append("Read: USART control"); break; + case 0x15: logger.error().append("Read: receiver status"); break; + case 0x16: logger.error().append("Read: transmitter status"); break; + case 0x17: logger.error().append("Read: USART data"); break; } return 0x00; } @@ -169,52 +170,58 @@ void MFP68901::write(int address, uint8_t value) { break; // USART block: TODO. - case 0x13: LOG("Write: sync character generator"); break; - case 0x14: LOG("Write: USART control"); break; - case 0x15: LOG("Write: receiver status"); break; - case 0x16: LOG("Write: transmitter status"); break; - case 0x17: LOG("Write: USART data"); break; + case 0x13: logger.error().append("Write: sync character generator"); break; + case 0x14: logger.error().append("Write: USART control"); break; + case 0x15: logger.error().append("Write: receiver status"); break; + case 0x16: logger.error().append("Write: transmitter status"); break; + case 0x17: logger.error().append("Write: USART data"); break; } update_clocking_observer(); } +template +void MFP68901::run_timer_for(int cycles) { + if(timers_[timer].mode >= TimerMode::Delay) { + // This code applies the timer prescaling only. prescale_count is used to count + // upwards rather than downwards for simplicity, but on the real hardware it's + // pretty safe to assume it actually counted downwards. So the clamp to 0 is + // because gymnastics may need to occur when the prescale value is altered, e.g. + // if a prescale of 256 is set and the prescale_count is currently 2 then the + // counter should roll over in 254 cycles. If the user at that point changes the + // prescale_count to 1 then the counter will need to be altered to -253 and + // allowed to keep counting up until it crosses both 0 and 1. + const int dividend = timers_[timer].prescale_count + cycles; + const int decrements = std::max(dividend / timers_[timer].prescale, 0); + if(decrements) { + decrement_timer(decrements); + timers_[timer].prescale_count = dividend % timers_[timer].prescale; + } else { + timers_[timer].prescale_count += cycles; + } + } +} + void MFP68901::run_for(HalfCycles time) { cycles_left_ += time; const int cycles = int(cycles_left_.flush().as_integral()); if(!cycles) return; - for(int c = 0; c < 4; ++c) { - if(timers_[c].mode >= TimerMode::Delay) { - // This code applies the timer prescaling only. prescale_count is used to count - // upwards rather than downwards for simplicity, but on the real hardware it's - // pretty safe to assume it actually counted downwards. So the clamp to 0 is - // because gymnastics may need to occur when the prescale value is altered, e.g. - // if a prescale of 256 is set and the prescale_count is currently 2 then the - // counter should roll over in 254 cycles. If the user at that point changes the - // prescale_count to 1 then the counter will need to be altered to -253 and - // allowed to keep counting up until it crosses both 0 and 1. - const int dividend = timers_[c].prescale_count + cycles; - const int decrements = std::max(dividend / timers_[c].prescale, 0); - if(decrements) { - decrement_timer(c, decrements); - timers_[c].prescale_count = dividend % timers_[c].prescale; - } else { - timers_[c].prescale_count += cycles; - } - } - } + run_timer_for<0>(cycles); + run_timer_for<1>(cycles); + run_timer_for<2>(cycles); + run_timer_for<3>(cycles); } -HalfCycles MFP68901::get_next_sequence_point() { +HalfCycles MFP68901::next_sequence_point() { return HalfCycles::max(); } // MARK: - Timers void MFP68901::set_timer_mode(int timer, TimerMode mode, int prescale, bool reset_timer) { - LOG("Timer " << timer << " mode set: " << int(mode) << "; prescale: " << prescale); + logger.error().append("Timer %d mode set: %d; prescale: %d", timer, mode, prescale); timers_[timer].mode = mode; if(reset_timer) { timers_[timer].prescale_count = 0; @@ -240,7 +247,8 @@ uint8_t MFP68901::get_timer_data(int timer) { return timers_[timer].value; } -void MFP68901::set_timer_event_input(int channel, bool value) { +template +void MFP68901::set_timer_event_input(bool value) { if(timers_[channel].event_input == value) return; timers_[channel].event_input = value; @@ -248,7 +256,7 @@ void MFP68901::set_timer_event_input(int channel, bool value) { // "The active state of the signal on TAI or TBI is dependent upon the associated // Interrupt Channel’s edge bit (GPIP 4 for TAI and GPIP 3 for TBI [...] ). // If the edge bit associated with the TAI or TBI input is a one, it will be active high. - decrement_timer(channel, 1); + decrement_timer(1); } // TODO: @@ -261,22 +269,48 @@ void MFP68901::set_timer_event_input(int channel, bool value) { // (the final bit probably explains 13 cycles of the DE to interrupt latency; not sure about the other ~15) } -void MFP68901::decrement_timer(int timer, int amount) { - while(amount--) { - --timers_[timer].value; - if(timers_[timer].value < 1) { - switch(timer) { - case 0: begin_interrupts(Interrupt::TimerA); break; - case 1: begin_interrupts(Interrupt::TimerB); break; - case 2: begin_interrupts(Interrupt::TimerC); break; - case 3: begin_interrupts(Interrupt::TimerD); break; - } +template void MFP68901::set_timer_event_input<0>(bool); +template void MFP68901::set_timer_event_input<1>(bool); +template void MFP68901::set_timer_event_input<2>(bool); +template void MFP68901::set_timer_event_input<3>(bool); - // Re: reloading when in event counting mode; I found the data sheet thoroughly unclear on - // this, but it appears empirically to be correct. See e.g. Pompey Pirates menu 27. - if(timers_[timer].mode == TimerMode::Delay || timers_[timer].mode == TimerMode::EventCount) { - timers_[timer].value += timers_[timer].reload_value; // TODO: properly. - } +template +void MFP68901::decrement_timer(int amount) { + while(amount) { + if(timers_[timer].value > amount) { + timers_[timer].value -= amount; + return; + } + + // Keep this check here to avoid the case where a decrement to zero occurs during one call to + // decrement_timer, triggering an interrupt, then the timer is already 0 at the next instance, + // causing a second interrupt. + // + // ... even though it would be nice to move it down below, after value has overtly been set to 0. + if(!timers_[timer].value) { + --timers_[timer].value; + --amount; + continue; + } + + // If here then amount is sufficient to, at least once, decrement the timer + // from 1 to 0. So there's an interrupt. + // + // (and, this switch is why this function is templated on timer ID) + switch(timer) { + case 0: begin_interrupts(Interrupt::TimerA); break; + case 1: begin_interrupts(Interrupt::TimerB); break; + case 2: begin_interrupts(Interrupt::TimerC); break; + case 3: begin_interrupts(Interrupt::TimerD); break; + } + + // Re: reloading when in event counting mode; I found the data sheet thoroughly unclear on + // this, but it appears empirically to be correct. See e.g. Pompey Pirates menu 27. + amount -= timers_[timer].value; + if(timers_[timer].mode == TimerMode::Delay || timers_[timer].mode == TimerMode::EventCount) { + timers_[timer].value = timers_[timer].reload_value; // TODO: properly. + } else { + timers_[timer].value = 0; } } } @@ -365,7 +399,7 @@ int MFP68901::acknowledge_interrupt() { int selected = 0; while((1 << selected) != mask) ++selected; -// LOG("Interrupt acknowledged: " << selected); +// logger.error().append("Interrupt acknowledged: %d", selected); return (interrupt_vector_ & 0xf0) | uint8_t(selected); } diff --git a/Components/68901/MFP68901.hpp b/Components/68901/MFP68901.hpp index 6f320a214..34f108d8c 100644 --- a/Components/68901/MFP68901.hpp +++ b/Components/68901/MFP68901.hpp @@ -6,15 +6,14 @@ // Copyright © 2019 Thomas Harte. All rights reserved. // -#ifndef MFP68901_hpp -#define MFP68901_hpp +#pragma once -#include #include "../../ClockReceiver/ClockReceiver.hpp" #include "../../ClockReceiver/ClockingHintSource.hpp" -namespace Motorola { -namespace MFP68901 { +#include + +namespace Motorola::MFP68901 { class PortHandler { public: @@ -40,10 +39,11 @@ class MFP68901: public ClockingHint::Source { /// so that mechanism can also be used to reduce the quantity of calls into this class. /// /// @discussion TODO, alas. - HalfCycles get_next_sequence_point(); + HalfCycles next_sequence_point(); /// Sets the current level of either of the timer event inputs — TAI and TBI in datasheet terms. - void set_timer_event_input(int channel, bool value); + template + void set_timer_event_input(bool value); /// Sets a port handler, a receiver that will be notified upon any change in GPIP output. /// @@ -86,7 +86,8 @@ class MFP68901: public ClockingHint::Source { void set_timer_mode(int timer, TimerMode, int prescale, bool reset_timer); void set_timer_data(int timer, uint8_t); uint8_t get_timer_data(int timer); - void decrement_timer(int timer, int amount); + template void decrement_timer(int amount); + template void run_timer_for(int cycles); struct Timer { TimerMode mode = TimerMode::Stopped; @@ -182,6 +183,3 @@ class MFP68901: public ClockingHint::Source { }; } -} - -#endif /* MFP68901_hpp */ diff --git a/Components/8255/i8255.hpp b/Components/8255/i8255.hpp index d8e2a42f6..8ea42f67b 100644 --- a/Components/8255/i8255.hpp +++ b/Components/8255/i8255.hpp @@ -6,13 +6,11 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef i8255_hpp -#define i8255_hpp +#pragma once #include -namespace Intel { -namespace i8255 { +namespace Intel::i8255 { class PortHandler { public: @@ -89,6 +87,3 @@ template class i8255 { }; } -} - -#endif /* i8255_hpp */ diff --git a/Components/8272/CommandDecoder.hpp b/Components/8272/CommandDecoder.hpp new file mode 100644 index 000000000..e0386c2aa --- /dev/null +++ b/Components/8272/CommandDecoder.hpp @@ -0,0 +1,218 @@ +// +// CommandDecoder.hpp +// Clock Signal +// +// Created by Thomas Harte on 24/11/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include +#include +#include + +namespace Intel::i8272 { + +enum class Command { + ReadData = 0x06, + ReadDeletedData = 0x0c, + + WriteData = 0x05, + WriteDeletedData = 0x09, + + ReadTrack = 0x02, + ReadID = 0x0a, + FormatTrack = 0x0d, + + ScanLow = 0x11, + ScanLowOrEqual = 0x19, + ScanHighOrEqual = 0x1d, + + Recalibrate = 0x07, + Seek = 0x0f, + + SenseInterruptStatus = 0x08, + Specify = 0x03, + SenseDriveStatus = 0x04, + + Invalid = 0x00, +}; + +class CommandDecoder { + public: + /// Add a byte to the current command. + void push_back(uint8_t byte) { + command_.push_back(byte); + } + + /// Reset decoding. + void clear() { + command_.clear(); + } + + /// @returns @c true if an entire command has been received; @c false if further bytes are needed. + bool has_command() const { + if(!command_.size()) { + return false; + } + + static constexpr std::size_t required_lengths[32] = { + 0, 0, 9, 3, 2, 9, 9, 2, + 1, 9, 2, 0, 9, 6, 0, 3, + 0, 9, 0, 0, 0, 0, 0, 0, + 0, 9, 0, 0, 0, 9, 0, 0, + }; + + return command_.size() >= required_lengths[command_[0] & 0x1f]; + } + + /// @returns The command requested. Valid only if @c has_command() is @c true. + Command command() const { + const auto command = Command(command_[0] & 0x1f); + + switch(command) { + case Command::ReadData: case Command::ReadDeletedData: + case Command::WriteData: case Command::WriteDeletedData: + case Command::ReadTrack: case Command::ReadID: + case Command::FormatTrack: + case Command::ScanLow: case Command::ScanLowOrEqual: + case Command::ScanHighOrEqual: + case Command::Recalibrate: case Command::Seek: + case Command::SenseInterruptStatus: + case Command::Specify: case Command::SenseDriveStatus: + return command; + + default: return Command::Invalid; + } + } + + // + // Commands that specify geometry; i.e. + // + // * ReadData; + // * ReadDeletedData; + // * WriteData; + // * WriteDeletedData; + // * ReadTrack; + // * ScanEqual; + // * ScanLowOrEqual; + // * ScanHighOrEqual. + // + + /// @returns @c true if this command specifies geometry, in which case geomtry() is well-defined. + /// @c false otherwise. + bool has_geometry() const { return command_.size() == 9; } + struct Geometry { + uint8_t cylinder, head, sector, size, end_of_track; + }; + Geometry geometry() const { + Geometry result; + result.cylinder = command_[2]; + result.head = command_[3]; + result.sector = command_[4]; + result.size = command_[5]; + result.end_of_track = command_[6]; + return result; + } + + // + // Commands that imply data access; i.e. + // + // * ReadData; + // * ReadDeletedData; + // * WriteData; + // * WriteDeletedData; + // * ReadTrack; + // * ReadID; + // * FormatTrack; + // * ScanLow; + // * ScanLowOrEqual; + // * ScanHighOrEqual. + // + + /// @returns @c true if this command involves reading or writing data, in which case target() will be valid. + /// @c false otherwise. + bool is_access() const { + switch(command()) { + case Command::ReadData: case Command::ReadDeletedData: + case Command::WriteData: case Command::WriteDeletedData: + case Command::ReadTrack: case Command::ReadID: + case Command::FormatTrack: + case Command::ScanLow: case Command::ScanLowOrEqual: + case Command::ScanHighOrEqual: + return true; + + default: + return false; + } + } + struct AccessTarget { + uint8_t drive, head; + bool mfm, skip_deleted; + }; + AccessTarget target() const { + AccessTarget result; + result.drive = command_[1] & 0x03; + result.head = (command_[1] >> 2) & 0x01; + result.mfm = command_[0] & 0x40; + result.skip_deleted = command_[0] & 0x20; + return result; + } + uint8_t drive_head() const { + return command_[1] & 7; + } + + // + // Command::FormatTrack + // + + struct FormatSpecs { + uint8_t bytes_per_sector; + uint8_t sectors_per_track; + uint8_t gap3_length; + uint8_t filler; + }; + FormatSpecs format_specs() const { + FormatSpecs result; + result.bytes_per_sector = command_[2]; + result.sectors_per_track = command_[3]; + result.gap3_length = command_[4]; + result.filler = command_[5]; + return result; + } + + // + // Command::Seek + // + + /// @returns The desired target track. + uint8_t seek_target() const { + return command_[2]; + } + + // + // Command::Specify + // + + struct SpecifySpecs { + // The below are all in milliseconds. + uint8_t step_rate_time; + uint8_t head_unload_time; + uint8_t head_load_time; + bool use_dma; + }; + SpecifySpecs specify_specs() const { + SpecifySpecs result; + result.step_rate_time = 16 - (command_[1] >> 4); // i.e. 1 to 16ms + result.head_unload_time = uint8_t((command_[1] & 0x0f) << 4); // i.e. 16 to 240ms + result.head_load_time = command_[2] & ~1; // i.e. 2 to 254 ms in increments of 2ms + result.use_dma = !(command_[2] & 1); + return result; + } + + private: + std::vector command_; +}; + +} diff --git a/Components/8272/Results.hpp b/Components/8272/Results.hpp new file mode 100644 index 000000000..13297afee --- /dev/null +++ b/Components/8272/Results.hpp @@ -0,0 +1,62 @@ +// +// Results.hpp +// Clock Signal +// +// Created by Thomas Harte on 27/11/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "CommandDecoder.hpp" +#include "Status.hpp" + +namespace Intel::i8272 { + +class Results { + public: + /// Serialises the response to Command::Invalid and Command::SenseInterruptStatus when no interrupt source was found. + void serialise_none() { + result_ = { 0x80 }; + } + + /// Serialises the response to Command::SenseInterruptStatus for a found drive. + void serialise(const Status &status, uint8_t cylinder) { + result_ = { cylinder, status[0] }; + } + + /// Serialises the seven-byte response to Command::SenseDriveStatus. + void serialise(uint8_t flags, uint8_t drive_side) { + result_ = { uint8_t(flags | drive_side) }; + } + + /// Serialises the response to: + /// + /// * Command::ReadData; + /// * Command::ReadDeletedData; + /// * Command::WriteData; + /// * Command::WriteDeletedData; + /// * Command::ReadID; + /// * Command::ReadTrack; + /// * Command::FormatTrack; + /// * Command::ScanLow; and + /// * Command::ScanHighOrEqual. + void serialise(const Status &status, uint8_t cylinder, uint8_t head, uint8_t sector, uint8_t size) { + result_ = { size, sector, head, cylinder, status[2], status[1], status[0] }; + } + + /// @returns @c true if all result bytes are exhausted; @c false otherwise. + bool empty() const { return result_.empty(); } + + /// @returns The next byte of the result. + uint8_t next() { + const uint8_t next = result_.back(); + result_.pop_back(); + return next; + } + + private: + std::vector result_; +}; + +} diff --git a/Components/8272/Status.hpp b/Components/8272/Status.hpp new file mode 100644 index 000000000..2b3f63be4 --- /dev/null +++ b/Components/8272/Status.hpp @@ -0,0 +1,131 @@ +// +// Status.hpp +// Clock Signal +// +// Created by Thomas Harte on 25/11/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +namespace Intel::i8272 { + +enum class MainStatus: uint8_t { + FDD0Seeking = 0x01, + FDD1Seeking = 0x02, + FDD2Seeking = 0x04, + FDD3Seeking = 0x08, + + CommandInProgress = 0x10, + InNonDMAExecution = 0x20, + DataIsToProcessor = 0x40, + DataReady = 0x80, +}; + +enum class Status0: uint8_t { + NormalTermination = 0x00, + AbnormalTermination = 0x80, + InvalidCommand = 0x40, + BecameNotReady = 0xc0, + + SeekEnded = 0x20, + EquipmentFault = 0x10, + NotReady = 0x08, + + HeadAddress = 0x04, + UnitSelect = 0x03, +}; + +enum class Status1: uint8_t { + EndOfCylinder = 0x80, + DataError = 0x20, + OverRun = 0x10, + NoData = 0x04, + NotWriteable = 0x02, + MissingAddressMark = 0x01, +}; + +enum class Status2: uint8_t { + DeletedControlMark = 0x40, + DataCRCError = 0x20, + WrongCyinder = 0x10, + ScanEqualHit = 0x08, + ScanNotSatisfied = 0x04, + BadCylinder = 0x02, + MissingDataAddressMark = 0x01, +}; + +enum class Status3: uint8_t { + Fault = 0x80, + WriteProtected = 0x40, + Ready = 0x20, + Track0 = 0x10, + TwoSided = 0x08, + HeadAddress = 0x04, + UnitSelect = 0x03, +}; + +class Status { + public: + Status() { + reset(); + } + + void reset() { + main_status_ = 0; + set(MainStatus::DataReady, true); + status_[0] = status_[1] = status_[2] = 0; + } + + /// @returns The main status register value. + uint8_t main() const { + return main_status_; + } + uint8_t operator [](int index) const { + return status_[index]; + } + + // + // Flag setters. + // + void set(MainStatus flag, bool value) { + set(uint8_t(flag), value, main_status_); + } + void start_seek(int drive) { main_status_ |= 1 << drive; } + void set(Status0 flag) { set(uint8_t(flag), true, status_[0]); } + void set(Status1 flag) { set(uint8_t(flag), true, status_[1]); } + void set(Status2 flag) { set(uint8_t(flag), true, status_[2]); } + + void set_status0(uint8_t value) { status_[0] = value; } + + // + // Flag getters. + // + bool get(MainStatus flag) { return main_status_ & uint8_t(flag); } + bool get(Status2 flag) { return status_[2] & uint8_t(flag); } + + /// Begin execution of whatever @c CommandDecoder currently describes, setting internal + /// state appropriately. + void begin(const CommandDecoder &command) { + set(MainStatus::DataReady, false); + set(MainStatus::CommandInProgress, true); + + if(command.is_access()) { + status_[0] = command.drive_head(); + } + } + + private: + void set(uint8_t flag, bool value, uint8_t &target) { + if(value) { + target |= flag; + } else { + target &= ~flag; + } + } + + uint8_t main_status_; + uint8_t status_[3]; +}; + +} diff --git a/Components/8272/i8272.cpp b/Components/8272/i8272.cpp index 95e735984..6f7592b79 100644 --- a/Components/8272/i8272.cpp +++ b/Components/8272/i8272.cpp @@ -10,72 +10,14 @@ #include "../../Outputs/Log.hpp" -using namespace Intel::i8272; - -#define SetDataRequest() (main_status_ |= 0x80) -#define ResetDataRequest() (main_status_ &= ~0x80) -#define DataRequest() (main_status_ & 0x80) - -#define SetDataDirectionToProcessor() (main_status_ |= 0x40) -#define SetDataDirectionFromProcessor() (main_status_ &= ~0x40) -#define DataDirectionToProcessor() (main_status_ & 0x40) - -#define SetNonDMAExecution() (main_status_ |= 0x20) -#define ResetNonDMAExecution() (main_status_ &= ~0x20) - -#define SetBusy() (main_status_ |= 0x10) -#define ResetBusy() (main_status_ &= ~0x10) -#define Busy() (main_status_ & 0x10) - -#define SetAbnormalTermination() (status_[0] |= 0x40) -#define SetInvalidCommand() (status_[0] |= 0x80) -#define SetReadyChanged() (status_[0] |= 0xc0) -#define SetSeekEnd() (status_[0] |= 0x20) -#define SetEquipmentCheck() (status_[0] |= 0x10) -#define SetNotReady() (status_[0] |= 0x08) -#define SetSide2() (status_[0] |= 0x04) - -#define SetEndOfCylinder() (status_[1] |= 0x80) -#define SetDataError() (status_[1] |= 0x20) -#define SetOverrun() (status_[1] |= 0x10) -#define SetNoData() (status_[1] |= 0x04) -#define SetNotWriteable() (status_[1] |= 0x02) -#define SetMissingAddressMark() (status_[1] |= 0x01) - -#define SetControlMark() (status_[2] |= 0x40) -#define ClearControlMark() (status_[2] &= ~0x40) -#define ControlMark() (status_[2] & 0x40) - -#define SetDataFieldDataError() (status_[2] |= 0x20) -#define SetWrongCyinder() (status_[2] |= 0x10) -#define SetScanEqualHit() (status_[2] |= 0x08) -#define SetScanNotSatisfied() (status_[2] |= 0x04) -#define SetBadCylinder() (status_[2] |= 0x02) -#define SetMissingDataAddressMark() (status_[2] |= 0x01) - namespace { - const uint8_t CommandReadData = 0x06; - const uint8_t CommandReadDeletedData = 0x0c; - const uint8_t CommandWriteData = 0x05; - const uint8_t CommandWriteDeletedData = 0x09; +Log::Logger logger; - const uint8_t CommandReadTrack = 0x02; - const uint8_t CommandReadID = 0x0a; - const uint8_t CommandFormatTrack = 0x0d; - - const uint8_t CommandScanLow = 0x11; - const uint8_t CommandScanLowOrEqual = 0x19; - const uint8_t CommandScanHighOrEqual = 0x1d; - - const uint8_t CommandRecalibrate = 0x07; - const uint8_t CommandSeek = 0x0f; - - const uint8_t CommandSenseInterruptStatus = 0x08; - const uint8_t CommandSpecify = 0x03; - const uint8_t CommandSenseDriveStatus = 0x04; } +using namespace Intel::i8272; + i8272::i8272(BusHandler &bus_handler, Cycles clock_rate) : Storage::Disk::MFMController(clock_rate), bus_handler_(bus_handler) { @@ -118,7 +60,7 @@ void i8272::run_for(Cycles cycles) { while(steps--) { // Perform a step. int direction = (drives_[c].target_head_position < drives_[c].head_position) ? -1 : 1; - LOG("Target " << PADDEC(0) << drives_[c].target_head_position << " versus believed " << int(drives_[c].head_position)); + logger.info().append("Target %d versus believed %d", drives_[c].target_head_position, drives_[c].head_position); select_drive(c); get_drive().step(Storage::Disk::HeadPosition(direction)); if(drives_[c].target_head_position >= 0) drives_[c].head_position += direction; @@ -172,12 +114,12 @@ void i8272::write(int address, uint8_t value) { if(!address) return; // if not ready for commands, do nothing - if(!DataRequest() || DataDirectionToProcessor()) return; + if(!status_.get(MainStatus::DataReady) || status_.get(MainStatus::DataIsToProcessor)) return; if(expects_input_) { input_ = value; has_input_ = true; - ResetDataRequest(); + status_.set(MainStatus::DataReady, false); } else { // accumulate latest byte in the command byte sequence command_.push_back(value); @@ -194,7 +136,7 @@ uint8_t i8272::read(int address) { return result; } else { - return main_status_; + return status_.main(); } } @@ -233,12 +175,11 @@ uint8_t i8272::read(int address) { if(distance_into_section_ < 6) goto CONCAT(read_header, __LINE__); \ #define SET_DRIVE_HEAD_MFM() \ - active_drive_ = command_[1]&3; \ - active_head_ = (command_[1] >> 2)&1; \ - status_[0] = (command_[1]&7); \ + active_drive_ = command_.target().drive; \ + active_head_ = command_.target().head; \ select_drive(active_drive_); \ get_drive().set_head(active_head_); \ - set_is_double_density(command_[0] & 0x40); + set_is_double_density(command_.target().mfm); #define WAIT_FOR_BYTES(n) \ distance_into_section_ = 0; \ @@ -270,7 +211,7 @@ uint8_t i8272::read(int address) { void i8272::posit_event(int event_type) { if(event_type == int(Event::IndexHole)) index_hole_count_++; if(event_type == int(Event8272::NoLongerReady)) { - SetNotReady(); + status_.set(Status0::NotReady); goto abort; } if(!(interesting_event_mask_ & event_type)) return; @@ -283,55 +224,32 @@ void i8272::posit_event(int event_type) { wait_for_command: expects_input_ = false; set_data_mode(Storage::Disk::MFMController::DataMode::Scanning); - ResetBusy(); - ResetNonDMAExecution(); + status_.set(MainStatus::CommandInProgress, false); + status_.set(MainStatus::InNonDMAExecution, false); command_.clear(); // Sets the data request bit, and waits for a byte. Then sets the busy bit. Continues accepting bytes // until it has a quantity that make up an entire command, then resets the data request bit and // branches to that command. wait_for_complete_command_sequence: - SetDataRequest(); - SetDataDirectionFromProcessor(); + status_.set(MainStatus::DataReady, true); + status_.set(MainStatus::DataIsToProcessor, false); WAIT_FOR_EVENT(Event8272::CommandByte) - SetBusy(); - static constexpr std::size_t required_lengths[32] = { - 0, 0, 9, 3, 2, 9, 9, 2, - 1, 9, 2, 0, 9, 6, 0, 3, - 0, 9, 0, 0, 0, 0, 0, 0, - 0, 9, 0, 0, 0, 9, 0, 0, - }; - - if(command_.size() < required_lengths[command_[0] & 0x1f]) goto wait_for_complete_command_sequence; - if(command_.size() == 9) { - cylinder_ = command_[2]; - head_ = command_[3]; - sector_ = command_[4]; - size_ = command_[5]; + if(!command_.has_command()) { + goto wait_for_complete_command_sequence; + } + + status_.begin(command_); + if(command_.has_geometry()) { + cylinder_ = command_.geometry().cylinder; + head_ = command_.geometry().head; + sector_ = command_.geometry().sector; + size_ = command_.geometry().size; } - ResetDataRequest(); - status_[0] = status_[1] = status_[2] = 0; // If this is not clearly a command that's safe to carry out in parallel to a seek, end all seeks. - switch(command_[0] & 0x1f) { - case CommandReadData: - case CommandReadDeletedData: - case CommandWriteData: - case CommandWriteDeletedData: - case CommandReadTrack: - case CommandReadID: - case CommandFormatTrack: - case CommandScanLow: - case CommandScanLowOrEqual: - case CommandScanHighOrEqual: - is_access_command_ = true; - break; - - default: - is_access_command_ = false; - break; - } + is_access_command_ = command_.is_access(); if(is_access_command_) { for(int c = 0; c < 4; c++) { @@ -343,39 +261,41 @@ void i8272::posit_event(int event_type) { // Establishes the drive and head being addressed, and whether in double density mode; populates the internal // cylinder, head, sector and size registers from the command stream. is_executing_ = true; - if(!dma_mode_) SetNonDMAExecution(); + if(!dma_mode_) { + status_.set(MainStatus::InNonDMAExecution, true); + } SET_DRIVE_HEAD_MFM(); LOAD_HEAD(); if(!get_drive().get_is_ready()) { - SetNotReady(); + status_.set(Status0::NotReady); goto abort; } } // Jump to the proper place. - switch(command_[0] & 0x1f) { - case CommandReadData: - case CommandReadDeletedData: + switch(command_.command()) { + case Command::ReadData: + case Command::ReadDeletedData: goto read_data; - case CommandWriteData: - case CommandWriteDeletedData: + case Command::WriteData: + case Command::WriteDeletedData: goto write_data; - case CommandReadTrack: goto read_track; - case CommandReadID: goto read_id; - case CommandFormatTrack: goto format_track; + case Command::ReadTrack: goto read_track; + case Command::ReadID: goto read_id; + case Command::FormatTrack: goto format_track; - case CommandScanLow: goto scan_low; - case CommandScanLowOrEqual: goto scan_low_or_equal; - case CommandScanHighOrEqual: goto scan_high_or_equal; + case Command::ScanLow: goto scan_low; + case Command::ScanLowOrEqual: goto scan_low_or_equal; + case Command::ScanHighOrEqual: goto scan_high_or_equal; - case CommandRecalibrate: goto recalibrate; - case CommandSeek: goto seek; + case Command::Recalibrate: goto recalibrate; + case Command::Seek: goto seek; - case CommandSenseInterruptStatus: goto sense_interrupt_status; - case CommandSpecify: goto specify; - case CommandSenseDriveStatus: goto sense_drive_status; + case Command::SenseInterruptStatus: goto sense_interrupt_status; + case Command::Specify: goto specify; + case Command::SenseDriveStatus: goto sense_drive_status; default: goto invalid; } @@ -389,52 +309,53 @@ void i8272::posit_event(int event_type) { // the index hole limit is breached or a sector is found with a cylinder, head, sector and size equal to the // values in the internal registers. index_hole_limit_ = 2; -// LOG("Seeking " << PADDEC(0) << cylinder_ << " " << head_ " " << sector_ << " " << size_); +// logger.info().append("Seeking " << PADDEC(0) << cylinder_ << " " << head_ " " << sector_ << " " << size_); find_next_sector: FIND_HEADER(); if(!index_hole_limit_) { // Two index holes have passed wihout finding the header sought. -// LOG("Not found"); - SetNoData(); +// logger.info().append("Not found"); + status_.set(Status1::NoData); goto abort; } index_hole_count_ = 0; -// LOG("Header"); +// logger.info().append("Header"); READ_HEADER(); if(index_hole_count_) { // This implies an index hole was sighted within the header. Error out. - SetEndOfCylinder(); + status_.set(Status1::EndOfCylinder); goto abort; } if(get_crc_generator().get_value()) { // This implies a CRC error in the header; mark as such but continue. - SetDataError(); + status_.set(Status1::DataError); } -// LOG("Considering << PADHEX(2) << header_[0] << " " << header_[1] << " " << header_[2] << " " << header_[3] << " [" << get_crc_generator().get_value() << "]"); +// logger.info().append("Considering %02x %02x %02x %02x [%04x]", header_[0], header_[1], header_[2], header_[3], get_crc_generator().get_value()); if(header_[0] != cylinder_ || header_[1] != head_ || header_[2] != sector_ || header_[3] != size_) goto find_next_sector; // Branch to whatever is supposed to happen next -// LOG("Proceeding"); - switch(command_[0] & 0x1f) { - case CommandReadData: - case CommandReadDeletedData: +// logger.info().append("Proceeding"); + switch(command_.command()) { + default: + case Command::ReadData: + case Command::ReadDeletedData: goto read_data_found_header; - case CommandWriteData: // write data - case CommandWriteDeletedData: // write deleted data + case Command::WriteData: // write data + case Command::WriteDeletedData: // write deleted data goto write_data_found_header; } // Performs the read data or read deleted data command. read_data: - LOG(PADHEX(2) << "Read [deleted] data [" - << int(command_[2]) << " " - << int(command_[3]) << " " - << int(command_[4]) << " " - << int(command_[5]) << " ... " - << int(command_[6]) << " " - << int(command_[8]) << "]"); +// logger.info().append("Read [deleted] data [%02x %02x %02x %02x ... %02x %02x]", +// command_[2], +// command_[3], +// command_[4], +// command_[5], +// command_[6], +// command_[8]); read_next_data: goto read_write_find_header; @@ -442,18 +363,18 @@ void i8272::posit_event(int event_type) { // flag doesn't match the sort the command was looking for. read_data_found_header: FIND_DATA(); - ClearControlMark(); + // TODO: should Status2::DeletedControlMark be cleared? if(event_type == int(Event::Token)) { if(get_latest_token().type != Token::Data && get_latest_token().type != Token::DeletedData) { // Something other than a data mark came next, impliedly an ID or index mark. - SetMissingAddressMark(); - SetMissingDataAddressMark(); + status_.set(Status1::MissingAddressMark); + status_.set(Status2::MissingDataAddressMark); goto abort; // TODO: or read_next_data? } else { - if((get_latest_token().type == Token::Data) != ((command_[0] & 0x1f) == CommandReadData)) { - if(!(command_[0]&0x20)) { + if((get_latest_token().type == Token::Data) != (command_.command() == Command::ReadData)) { + if(!command_.target().skip_deleted) { // SK is not set; set the error flag but read this sector before finishing. - SetControlMark(); + status_.set(Status2::DeletedControlMark); } else { // SK is set; skip this sector. goto read_next_data; @@ -462,7 +383,7 @@ void i8272::posit_event(int event_type) { } } else { // An index hole appeared before the data mark. - SetEndOfCylinder(); + status_.set(Status1::EndOfCylinder); goto abort; // TODO: or read_next_data? } @@ -478,21 +399,21 @@ void i8272::posit_event(int event_type) { if(event_type == int(Event::Token)) { result_stack_.push_back(get_latest_token().byte_value); distance_into_section_++; - SetDataRequest(); - SetDataDirectionToProcessor(); + status_.set(MainStatus::DataReady, true); + status_.set(MainStatus::DataIsToProcessor, true); WAIT_FOR_EVENT(int(Event8272::ResultEmpty) | int(Event::Token) | int(Event::IndexHole)); } switch(event_type) { case int(Event8272::ResultEmpty): // The caller read the byte in time; proceed as normal. - ResetDataRequest(); + status_.set(MainStatus::DataReady, false); if(distance_into_section_ < (128 << size_)) goto read_data_get_byte; break; case int(Event::Token): // The caller hasn't read the old byte yet and a new one has arrived - SetOverrun(); + status_.set(Status1::OverRun); goto abort; break; case int(Event::IndexHole): - SetEndOfCylinder(); + status_.set(Status1::EndOfCylinder); goto abort; break; } @@ -502,14 +423,14 @@ void i8272::posit_event(int event_type) { WAIT_FOR_EVENT(Event::Token); if(get_crc_generator().get_value()) { // This implies a CRC error in the sector; mark as such and temrinate. - SetDataError(); - SetDataFieldDataError(); + status_.set(Status1::DataError); + status_.set(Status2::DataCRCError); goto abort; } // check whether that's it: either the final requested sector has been read, or because // a sector that was [/wasn't] marked as deleted when it shouldn't [/should] have been - if(sector_ != command_[6] && !ControlMark()) { + if(sector_ != command_.geometry().end_of_track && !status_.get(Status2::DeletedControlMark)) { sector_++; goto read_next_data; } @@ -518,16 +439,16 @@ void i8272::posit_event(int event_type) { goto post_st012chrn; write_data: - LOG(PADHEX(2) << "Write [deleted] data [" - << int(command_[2]) << " " - << int(command_[3]) << " " - << int(command_[4]) << " " - << int(command_[5]) << " ... " - << int(command_[6]) << " " - << int(command_[8]) << "]"); +// logger.info().append("Write [deleted] data [%02x %02x %02x %02x ... %02x %02x]", +// command_[2], +// command_[3], +// command_[4], +// command_[5], +// command_[6], +// command_[8]); if(get_drive().get_is_read_only()) { - SetNotWriteable(); + status_.set(Status1::NotWriteable); goto abort; } @@ -538,34 +459,34 @@ void i8272::posit_event(int event_type) { WAIT_FOR_BYTES(get_is_double_density() ? 22 : 11); begin_writing(true); - write_id_data_joiner((command_[0] & 0x1f) == CommandWriteDeletedData, true); + write_id_data_joiner(command_.command() == Command::WriteDeletedData, true); - SetDataDirectionFromProcessor(); - SetDataRequest(); + status_.set(MainStatus::DataIsToProcessor, false); + status_.set(MainStatus::DataReady, true); expects_input_ = true; distance_into_section_ = 0; write_loop: WAIT_FOR_EVENT(Event::DataWritten); if(!has_input_) { - SetOverrun(); + status_.set(Status1::OverRun); goto abort; } write_byte(input_); has_input_ = false; distance_into_section_++; if(distance_into_section_ < (128 << size_)) { - SetDataRequest(); + status_.set(MainStatus::DataReady, true); goto write_loop; } - LOG("Wrote " << PADDEC(0) << distance_into_section_ << " bytes"); + logger.info().append("Wrote %d bytes", distance_into_section_); write_crc(); expects_input_ = false; WAIT_FOR_EVENT(Event::DataWritten); end_writing(); - if(sector_ != command_[6]) { + if(sector_ != command_.geometry().end_of_track) { sector_++; goto write_next_data; } @@ -575,14 +496,14 @@ void i8272::posit_event(int event_type) { // Performs the read ID command. read_id: // Establishes the drive and head being addressed, and whether in double density mode. - LOG(PADHEX(2) << "Read ID [" << int(command_[0]) << " " << int(command_[1]) << "]"); +// logger.info().append("Read ID [%02x %02x]", command_[0], command_[1]); // Sets a maximum index hole limit of 2 then waits either until it finds a header mark or sees too many index holes. // If a header mark is found, reads in the following bytes that produce a header. Otherwise branches to data not found. index_hole_limit_ = 2; FIND_HEADER(); if(!index_hole_limit_) { - SetMissingAddressMark(); + status_.set(Status1::MissingAddressMark); goto abort; } READ_HEADER(); @@ -597,11 +518,11 @@ void i8272::posit_event(int event_type) { // Performs read track. read_track: - LOG(PADHEX(2) << "Read track [" - << int(command_[2]) << " " - << int(command_[3]) << " " - << int(command_[4]) << " " - << int(command_[5]) << "]"); +// logger.info().append("Read track [%02x %02x %02x %02x]" +// command_[2], +// command_[3], +// command_[4], +// command_[5]); // Wait for the index hole. WAIT_FOR_EVENT(Event::IndexHole); @@ -614,7 +535,7 @@ void i8272::posit_event(int event_type) { FIND_HEADER(); if(!index_hole_limit_) { if(!sector_) { - SetMissingAddressMark(); + status_.set(Status1::MissingAddressMark); goto abort; } else { goto post_st012chrn; @@ -624,27 +545,27 @@ void i8272::posit_event(int event_type) { FIND_DATA(); distance_into_section_ = 0; - SetDataDirectionToProcessor(); + status_.set(MainStatus::DataIsToProcessor, true); read_track_get_byte: WAIT_FOR_EVENT(Event::Token); result_stack_.push_back(get_latest_token().byte_value); distance_into_section_++; - SetDataRequest(); + status_.set(MainStatus::DataReady, true); // TODO: other possible exit conditions; find a way to merge with the read_data version of this. WAIT_FOR_EVENT(int(Event8272::ResultEmpty)); - ResetDataRequest(); + status_.set(MainStatus::DataReady, false); if(distance_into_section_ < (128 << header_[2])) goto read_track_get_byte; sector_++; - if(sector_ < command_[6]) goto read_track_next_sector; + if(sector_ < command_.geometry().end_of_track) goto read_track_next_sector; goto post_st012chrn; // Performs format [/write] track. format_track: - LOG("Format track"); + logger.info().append("Format track"); if(get_drive().get_is_read_only()) { - SetNotWriteable(); + status_.set(Status1::NotWriteable); goto abort; } @@ -663,15 +584,15 @@ void i8272::posit_event(int event_type) { // Write the sector header, obtaining its contents // from the processor. - SetDataDirectionFromProcessor(); - SetDataRequest(); + status_.set(MainStatus::DataIsToProcessor, false); + status_.set(MainStatus::DataReady, true); expects_input_ = true; distance_into_section_ = 0; format_track_write_header: WAIT_FOR_EVENT(int(Event::DataWritten) | int(Event::IndexHole)); switch(event_type) { case int(Event::IndexHole): - SetOverrun(); + status_.set(Status1::OverRun); goto abort; break; case int(Event::DataWritten): @@ -680,31 +601,27 @@ void i8272::posit_event(int event_type) { has_input_ = false; distance_into_section_++; if(distance_into_section_ < 4) { - SetDataRequest(); + status_.set(MainStatus::DataReady, true); goto format_track_write_header; } break; } - LOG(PADHEX(2) << "W:" - << int(header_[0]) << " " - << int(header_[1]) << " " - << int(header_[2]) << " " - << int(header_[3]) << ", " - << get_crc_generator().get_value()); + logger.info().append("W: %02x %02x %02x %02x, %04x", + header_[0], header_[1], header_[2], header_[3], get_crc_generator().get_value()); write_crc(); // Write the sector body. write_id_data_joiner(false, false); - write_n_bytes(128 << command_[2], command_[5]); + write_n_bytes(128 << command_.format_specs().bytes_per_sector, command_.format_specs().filler); write_crc(); // Write the prescribed gap. - write_n_bytes(command_[4], get_is_double_density() ? 0x4e : 0xff); + write_n_bytes(command_.format_specs().gap3_length, get_is_double_density() ? 0x4e : 0xff); // Consider repeating. sector_++; - if(sector_ < command_[3] && !index_hole_count_) + if(sector_ < command_.format_specs().sectors_per_track && !index_hole_count_) goto format_track_write_sector; // Otherwise, pad out to the index hole. @@ -723,15 +640,15 @@ void i8272::posit_event(int event_type) { goto post_st012chrn; scan_low: - ERROR("Scan low unimplemented!!"); + logger.error().append("Scan low unimplemented!!"); goto wait_for_command; scan_low_or_equal: - ERROR("Scan low or equal unimplemented!!"); + logger.error().append("Scan low or equal unimplemented!!"); goto wait_for_command; scan_high_or_equal: - ERROR("Scan high or equal unimplemented!!"); + logger.error().append("Scan high or equal unimplemented!!"); goto wait_for_command; // Performs both recalibrate and seek commands. These commands occur asynchronously, so the actual work @@ -739,7 +656,7 @@ void i8272::posit_event(int event_type) { recalibrate: seek: { - int drive = command_[1]&3; + const int drive = command_.target().drive; select_drive(drive); // Increment the seeking count if this drive wasn't already seeking. @@ -755,18 +672,18 @@ void i8272::posit_event(int event_type) { drives_[drive].step_rate_counter = 8000 * step_rate_time_; drives_[drive].steps_taken = 0; drives_[drive].seek_failed = false; - main_status_ |= 1 << (command_[1]&3); + status_.start_seek(command_.target().drive); // If this is a seek, set the processor-supplied target location; otherwise it is a recalibrate, // which means resetting the current state now but aiming to hit '-1' (which the stepping code // up in run_for understands to mean 'keep going until track 0 is active'). - if(command_.size() > 2) { - drives_[drive].target_head_position = command_[2]; - LOG(PADHEX(2) << "Seek to " << int(command_[2])); + if(command_.command() != Command::Recalibrate) { + drives_[drive].target_head_position = command_.seek_target(); + logger.info().append("Seek to %d", command_.seek_target()); } else { drives_[drive].target_head_position = -1; drives_[drive].head_position = 0; - LOG("Recalibrate"); + logger.info().append("Recalibrate"); } // Check whether any steps are even needed; if not then mark as completed already. @@ -779,7 +696,7 @@ void i8272::posit_event(int event_type) { // Performs sense interrupt status. sense_interrupt_status: - LOG("Sense interrupt status"); + logger.info().append("Sense interrupt status"); { // Find the first drive that is in the CompletedSeeking state. int found_drive = -1; @@ -793,9 +710,9 @@ void i8272::posit_event(int event_type) { // If a drive was found, return its results. Otherwise return a single 0x80. if(found_drive != -1) { drives_[found_drive].phase = Drive::NotSeeking; - status_[0] = uint8_t(found_drive); - main_status_ &= ~(1 << found_drive); - SetSeekEnd(); + status_.set_status0(uint8_t(found_drive | uint8_t(Status0::SeekEnded))); +// status_.end_sense_interrupt_status(found_drive, 0); +// status_.set(Status0::SeekEnded); result_stack_ = { drives_[found_drive].head_position, status_[0]}; } else { @@ -807,25 +724,25 @@ void i8272::posit_event(int event_type) { // Performs specify. specify: // Just store the values, and terminate the command. - LOG("Specify"); - step_rate_time_ = 16 - (command_[1] >> 4); // i.e. 1 to 16ms - head_unload_time_ = (command_[1] & 0x0f) << 4; // i.e. 16 to 240ms - head_load_time_ = command_[2] & ~1; // i.e. 2 to 254 ms in increments of 2ms + logger.info().append("Specify"); + step_rate_time_ = command_.specify_specs().step_rate_time; + head_unload_time_ = command_.specify_specs().head_unload_time; + head_load_time_ = command_.specify_specs().head_load_time; if(!head_unload_time_) head_unload_time_ = 16; if(!head_load_time_) head_load_time_ = 2; - dma_mode_ = !(command_[2] & 1); + dma_mode_ = command_.specify_specs().use_dma; goto wait_for_command; sense_drive_status: - LOG("Sense drive status"); + logger.info().append("Sense drive status"); { - int drive = command_[1] & 3; + int drive = command_.target().drive; select_drive(drive); - result_stack_= { + result_stack_ = { uint8_t( - (command_[1] & 7) | // drive and head number - 0x08 | // single sided + (command_.drive_head()) | // drive and head number + 0x08 | // single sided (get_drive().get_is_track_zero() ? 0x10 : 0x00) | (get_drive().get_is_ready() ? 0x20 : 0x00) | (get_drive().get_is_read_only() ? 0x40 : 0x00) @@ -843,7 +760,7 @@ void i8272::posit_event(int event_type) { // Sets abnormal termination of the current command and proceeds to an ST0, ST1, ST2, C, H, R, N result phase. abort: end_writing(); - SetAbnormalTermination(); + status_.set(Status0::AbnormalTermination); goto post_st012chrn; // Posts ST0, ST1, ST2, C, H, R and N as a result phase. @@ -857,17 +774,19 @@ void i8272::posit_event(int event_type) { // Posts whatever is in result_stack_ as a result phase. Be aware that it is a stack, so the // last thing in it will be returned first. post_result: - LOGNBR(PADHEX(2) << "Result to " << int(command_[0] & 0x1f) << ", main " << int(main_status_) << "; "); - for(std::size_t c = 0; c < result_stack_.size(); c++) { - LOGNBR(" " << int(result_stack_[result_stack_.size() - 1 - c])); - } - LOGNBR(std::endl); +// { +// auto line = logger.info(); +// line.append("Result to %02x, main %02x", command_[0] & 0x1f, main_status_); +// for(std::size_t c = 0; c < result_stack_.size(); c++) { +// line.append(" %02x", result_stack_[result_stack_.size() - 1 - c]); +// } +// } // Set ready to send data to the processor, no longer in non-DMA execution phase. is_executing_ = false; - ResetNonDMAExecution(); - SetDataRequest(); - SetDataDirectionToProcessor(); + status_.set(MainStatus::InNonDMAExecution, false); + status_.set(MainStatus::DataReady, true); + status_.set(MainStatus::DataIsToProcessor, true); // The actual stuff of unwinding result_stack_ is handled by ::read; wait // until the processor has read all result bytes. diff --git a/Components/8272/i8272.hpp b/Components/8272/i8272.hpp index cb589343c..314482895 100644 --- a/Components/8272/i8272.hpp +++ b/Components/8272/i8272.hpp @@ -6,8 +6,10 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef i8272_hpp -#define i8272_hpp +#pragma once + +#include "CommandDecoder.hpp" +#include "Status.hpp" #include "../../Storage/Disk/Controller/MFMDiskController.hpp" @@ -15,8 +17,7 @@ #include #include -namespace Intel { -namespace i8272 { +namespace Intel::i8272 { class BusHandler { public: @@ -51,11 +52,12 @@ class i8272 : public Storage::Disk::MFMController { std::unique_ptr allocated_bus_handler_; // Status registers. - uint8_t main_status_ = 0; - uint8_t status_[3] = {0, 0, 0}; + Status status_; - // A buffer for accumulating the incoming command, and one for accumulating the result. - std::vector command_; + // The incoming command. + CommandDecoder command_; + + // A buffer to accumulate the result. std::vector result_stack_; uint8_t input_ = 0; bool has_input_ = false; @@ -131,6 +133,3 @@ class i8272 : public Storage::Disk::MFMController { }; } -} - -#endif /* i8272_hpp */ diff --git a/Components/8530/z8530.cpp b/Components/8530/z8530.cpp index a54b9451c..8077f0df6 100644 --- a/Components/8530/z8530.cpp +++ b/Components/8530/z8530.cpp @@ -8,15 +8,16 @@ #include "z8530.hpp" -#ifndef NDEBUG -#define NDEBUG -#endif - -#define LOG_PREFIX "[SCC] " #include "../../Outputs/Log.hpp" using namespace Zilog::SCC; +namespace { + +Log::Logger log; + +} + void z8530::reset() { // TODO. } @@ -53,7 +54,7 @@ std::uint8_t z8530::read(int address) { case 2: // Handled non-symmetrically between channels. if(address & 1) { - LOG("Unimplemented: register 2 status bits"); + log.error().append("Unimplemented: register 2 status bits"); } else { result = interrupt_vector_; @@ -110,11 +111,11 @@ void z8530::write(int address, std::uint8_t value) { case 2: // Interrupt vector register; used only by Channel B. // So there's only one of these. interrupt_vector_ = value; - LOG("Interrupt vector set to " << PADHEX(2) << int(value)); + log.info().append("Interrupt vector set to %d", value); break; case 9: // Master interrupt and reset register; there is also only one of these. - LOG("Master interrupt and reset register: " << PADHEX(2) << int(value)); + log.info().append("Master interrupt and reset register: %02x", value); master_interrupt_control_ = value; break; } @@ -151,7 +152,7 @@ uint8_t z8530::Channel::read(bool data, uint8_t pointer) { if(data) { return data_; } else { - LOG("Control read from register " << int(pointer)); + log.info().append("Control read from register %d", pointer); // Otherwise, this is a control read... switch(pointer) { default: @@ -236,10 +237,10 @@ void z8530::Channel::write(bool data, uint8_t pointer, uint8_t value) { data_ = value; return; } else { - LOG("Control write: " << PADHEX(2) << int(value) << " to register " << int(pointer)); + log.info().append("Control write: %02x to register %d", value, pointer); switch(pointer) { default: - LOG("Unrecognised control write: " << PADHEX(2) << int(value) << " to register " << int(pointer)); + log.info().append("Unrecognised control write: %02x to register %d", value, pointer); break; case 0x0: // Write register 0 — CRC reset and other functions. @@ -247,13 +248,13 @@ void z8530::Channel::write(bool data, uint8_t pointer, uint8_t value) { switch(value >> 6) { default: /* Do nothing. */ break; case 1: - LOG("TODO: reset Rx CRC checker."); + log.error().append("TODO: reset Rx CRC checker."); break; case 2: - LOG("TODO: reset Tx CRC checker."); + log.error().append("TODO: reset Tx CRC checker."); break; case 3: - LOG("TODO: reset Tx underrun/EOM latch."); + log.error().append("TODO: reset Tx underrun/EOM latch."); break; } @@ -261,24 +262,24 @@ void z8530::Channel::write(bool data, uint8_t pointer, uint8_t value) { switch((value >> 3)&7) { default: /* Do nothing. */ break; case 2: -// LOG("reset ext/status interrupts."); +// log.info().append("reset ext/status interrupts."); external_status_interrupt_ = false; external_interrupt_status_ = 0; break; case 3: - LOG("TODO: send abort (SDLC)."); + log.error().append("TODO: send abort (SDLC)."); break; case 4: - LOG("TODO: enable interrupt on next Rx character."); + log.error().append("TODO: enable interrupt on next Rx character."); break; case 5: - LOG("TODO: reset Tx interrupt pending."); + log.error().append("TODO: reset Tx interrupt pending."); break; case 6: - LOG("TODO: reset error."); + log.error().append("TODO: reset error."); break; case 7: - LOG("TODO: reset highest IUS."); + log.error().append("TODO: reset highest IUS."); break; } break; @@ -303,7 +304,7 @@ void z8530::Channel::write(bool data, uint8_t pointer, uint8_t value) { b1 = 1 => transmit buffer empty interrupt is enabled; 0 => it isn't. b0 = 1 => external interrupt is enabled; 0 => it isn't. */ - LOG("Interrupt mask: " << PADHEX(2) << int(value)); + log.info().append("Interrupt mask: %02x", value); break; case 0x2: // Write register 2 - interrupt vector. @@ -318,9 +319,7 @@ void z8530::Channel::write(bool data, uint8_t pointer, uint8_t value) { case 2: receive_bit_count = 6; break; case 3: receive_bit_count = 8; break; } - LOG("Receive bit count: " << receive_bit_count); - - (void)receive_bit_count; + log.info().append("Receive bit count: %d", receive_bit_count); /* b7,b6: diff --git a/Components/8530/z8530.hpp b/Components/8530/z8530.hpp index 4826cb445..422074634 100644 --- a/Components/8530/z8530.hpp +++ b/Components/8530/z8530.hpp @@ -6,13 +6,11 @@ // Copyright © 2019 Thomas Harte. All rights reserved. // -#ifndef z8530_hpp -#define z8530_hpp +#pragma once #include -namespace Zilog { -namespace SCC { +namespace Zilog::SCC { /*! Models the Zilog 8530 SCC, a serial adaptor. @@ -110,7 +108,3 @@ class z8530 { }; } -} - - -#endif /* z8530_hpp */ diff --git a/Components/9918/9918.cpp b/Components/9918/9918.cpp deleted file mode 100644 index aef510c59..000000000 --- a/Components/9918/9918.cpp +++ /dev/null @@ -1,1052 +0,0 @@ -// -// 9918.cpp -// Clock Signal -// -// Created by Thomas Harte on 25/11/2017. -// Copyright 2017 Thomas Harte. All rights reserved. -// - -#include "9918.hpp" - -#include -#include -#include -#include "../../Outputs/Log.hpp" - -using namespace TI::TMS; - -namespace { - -constexpr uint8_t StatusInterrupt = 0x80; -constexpr uint8_t StatusSpriteOverflow = 0x40; - -constexpr int StatusSpriteCollisionShift = 5; -constexpr uint8_t StatusSpriteCollision = 0x20; - -// 342 internal cycles are 228/227.5ths of a line, so 341.25 cycles should be a whole -// line. Therefore multiply everything by four, but set line length to 1365 rather than 342*4 = 1368. -constexpr unsigned int CRTCyclesPerLine = 1365; -constexpr unsigned int CRTCyclesDivider = 4; - -struct ReverseTable { - std::uint8_t map[256]; - - ReverseTable() { - for(int c = 0; c < 256; ++c) { - map[c] = uint8_t( - ((c & 0x80) >> 7) | - ((c & 0x40) >> 5) | - ((c & 0x20) >> 3) | - ((c & 0x10) >> 1) | - ((c & 0x08) << 1) | - ((c & 0x04) << 3) | - ((c & 0x02) << 5) | - ((c & 0x01) << 7) - ); - } - } -} reverse_table; - -} - -Base::Base(Personality p) : - personality_(p), - crt_(CRTCyclesPerLine, CRTCyclesDivider, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Red8Green8Blue8) { - // Unimaginatively, this class just passes RGB through to the shader. Investigation is needed - // into whether there's a more natural form. It feels unlikely given the diversity of chips modelled. - - switch(p) { - case TI::TMS::TMS9918A: - case TI::TMS::SMSVDP: - case TI::TMS::SMS2VDP: - case TI::TMS::GGVDP: - ram_.resize(16 * 1024); - break; - case TI::TMS::V9938: - ram_.resize(128 * 1024); - break; - case TI::TMS::V9958: - ram_.resize(192 * 1024); - break; - } - - if(is_sega_vdp(personality_)) { - mode_timing_.line_interrupt_position = 64; - - mode_timing_.end_of_frame_interrupt_position.column = 63; - mode_timing_.end_of_frame_interrupt_position.row = 193; - } - - // Establish that output is delayed after reading by `output_lag` cycles; start - // at a random position. - read_pointer_.row = rand() % 262; - read_pointer_.column = rand() % (342 - output_lag); - write_pointer_.row = read_pointer_.row; - write_pointer_.column = read_pointer_.column + output_lag; -} - -TMS9918::TMS9918(Personality p): - Base(p) { - crt_.set_display_type(Outputs::Display::DisplayType::RGB); - crt_.set_visible_area(Outputs::Display::Rect(0.07f, 0.0375f, 0.875f, 0.875f)); - - // The TMS remains in-phase with the NTSC colour clock; this is an empirical measurement - // intended to produce the correct relationship between the hard edges between pixels and - // the colour clock. It was eyeballed rather than derived from any knowledge of the TMS - // colour burst generator because I've yet to find any. - crt_.set_immediate_default_phase(0.85f); -} - -void TMS9918::set_tv_standard(TVStandard standard) { - tv_standard_ = standard; - switch(standard) { - case TVStandard::PAL: - mode_timing_.total_lines = 313; - mode_timing_.first_vsync_line = 253; - crt_.set_new_display_type(CRTCyclesPerLine, Outputs::Display::Type::PAL50); - break; - default: - mode_timing_.total_lines = 262; - mode_timing_.first_vsync_line = 227; - crt_.set_new_display_type(CRTCyclesPerLine, Outputs::Display::Type::NTSC60); - break; - } -} - -void TMS9918::set_scan_target(Outputs::Display::ScanTarget *scan_target) { - crt_.set_scan_target(scan_target); -} - -Outputs::Display::ScanStatus TMS9918::get_scaled_scan_status() const { - // The input was scaled by 3/4 to convert half cycles to internal ticks, - // so undo that and also allow for: (i) the multiply by 4 that it takes - // to reach the CRT; and (ii) the fact that the half-cycles value was scaled, - // and this should really reply in whole cycles. - return crt_.get_scaled_scan_status() * (4.0f / (3.0f * 8.0f)); -} - -void TMS9918::set_display_type(Outputs::Display::DisplayType display_type) { - crt_.set_display_type(display_type); -} - -Outputs::Display::DisplayType TMS9918::get_display_type() const { - return crt_.get_display_type(); -} - -void Base::LineBuffer::reset_sprite_collection() { - sprites_stopped = false; - active_sprite_slot = 0; - - for(int c = 0; c < 8; ++c) { - active_sprites[c].shift_position = 0; - } -} - -void Base::posit_sprite(LineBuffer &buffer, int sprite_number, int sprite_position, int screen_row) { - if(!(status_ & StatusSpriteOverflow)) { - status_ = uint8_t((status_ & ~0x1f) | (sprite_number & 0x1f)); - } - if(buffer.sprites_stopped) - return; - - // A sprite Y of 208 means "don't scan the list any further". - if(mode_timing_.allow_sprite_terminator && sprite_position == mode_timing_.sprite_terminator) { - buffer.sprites_stopped = true; - return; - } - - const int sprite_row = (((screen_row + 1) % mode_timing_.total_lines) - ((sprite_position + 1) & 255)) & 255; - if(sprite_row < 0 || sprite_row >= sprite_height_) return; - - if(buffer.active_sprite_slot == mode_timing_.maximum_visible_sprites) { - status_ |= StatusSpriteOverflow; - return; - } - - LineBuffer::ActiveSprite &sprite = buffer.active_sprites[buffer.active_sprite_slot]; - sprite.index = sprite_number; - sprite.row = sprite_row >> (sprites_magnified_ ? 1 : 0); - ++buffer.active_sprite_slot; -} - -void TMS9918::run_for(const HalfCycles cycles) { - // As specific as I've been able to get: - // Scanline time is always 228 cycles. - // PAL output is 313 lines total. NTSC output is 262 lines total. - // Interrupt is signalled upon entering the lower border. - - // Convert 456 clocked half cycles per line to 342 internal cycles per line; - // the internal clock is 1.5 times the nominal 3.579545 Mhz that I've advertised - // for this part. So multiply by three quarters. - int int_cycles = int(cycles.as_integral() * 3) + cycles_error_; - cycles_error_ = int_cycles & 3; - int_cycles >>= 2; - if(!int_cycles) return; - - // There are two intertwined processes here, 'writing' (which means writing to the - // line buffers, i.e. it's everything to do with collecting a line) and 'reading' - // (which means reading from the line buffers and generating video). - int write_cycles_pool = int_cycles; - int read_cycles_pool = int_cycles; - - while(write_cycles_pool || read_cycles_pool) { -#ifndef NDEBUG - LineBufferPointer backup = read_pointer_; -#endif - - if(write_cycles_pool) { - // Determine how much writing to do. - const int write_cycles = std::min(342 - write_pointer_.column, write_cycles_pool); - const int end_column = write_pointer_.column + write_cycles; - LineBuffer &line_buffer = line_buffers_[write_pointer_.row]; - - // Determine what this does to any enqueued VRAM access. - minimum_access_column_ = write_pointer_.column + cycles_until_access_; - cycles_until_access_ -= write_cycles; - - - // --------------------------------------- - // Latch scrolling position, if necessary. - // --------------------------------------- - if(is_sega_vdp(personality_)) { - if(write_pointer_.column < 61 && end_column >= 61) { - if(!write_pointer_.row) { - master_system_.latched_vertical_scroll = master_system_.vertical_scroll; - - if(master_system_.mode4_enable) { - mode_timing_.pixel_lines = 192; - if(mode2_enable_ && mode1_enable_) mode_timing_.pixel_lines = 224; - if(mode2_enable_ && mode3_enable_) mode_timing_.pixel_lines = 240; - - mode_timing_.allow_sprite_terminator = mode_timing_.pixel_lines == 192; - mode_timing_.first_vsync_line = (mode_timing_.total_lines + mode_timing_.pixel_lines) >> 1; - - mode_timing_.end_of_frame_interrupt_position.row = mode_timing_.pixel_lines + 1; - } - } - line_buffer.latched_horizontal_scroll = master_system_.horizontal_scroll; - } - } - - - - // ------------------------ - // Perform memory accesses. - // ------------------------ -#define fetch(function) \ - if(final_window != 171) { \ - function(first_window, final_window);\ - } else {\ - function(first_window, final_window);\ - } - - // column_ and end_column are in 342-per-line cycles; - // adjust them to a count of windows. - const int first_window = write_pointer_.column >> 1; - const int final_window = end_column >> 1; - if(first_window != final_window) { - switch(line_buffer.line_mode) { - case LineMode::Text: fetch(fetch_tms_text); break; - case LineMode::Character: fetch(fetch_tms_character); break; - case LineMode::SMS: fetch(fetch_sms); break; - case LineMode::Refresh: fetch(fetch_tms_refresh); break; - } - } - -#undef fetch - - - - // ------------------------------- - // Check for interrupt conditions. - // ------------------------------- - if(write_pointer_.column < mode_timing_.line_interrupt_position && end_column >= mode_timing_.line_interrupt_position) { - // The Sega VDP offers a decrementing counter for triggering line interrupts; - // it is reloaded either when it overflows or upon every non-pixel line after the first. - // It is otherwise decremented. - if(is_sega_vdp(personality_)) { - if(write_pointer_.row >= 0 && write_pointer_.row <= mode_timing_.pixel_lines) { - --line_interrupt_counter; - if(line_interrupt_counter == 0xff) { - line_interrupt_pending_ = true; - line_interrupt_counter = line_interrupt_target; - } - } else { - line_interrupt_counter = line_interrupt_target; - } - } - - // TODO: the V9938 provides line interrupts from direct specification of the target line. - // So life is easy. - } - - if( - write_pointer_.row == mode_timing_.end_of_frame_interrupt_position.row && - write_pointer_.column < mode_timing_.end_of_frame_interrupt_position.column && - end_column >= mode_timing_.end_of_frame_interrupt_position.column - ) { - status_ |= StatusInterrupt; - } - - - - // ------------- - // Advance time. - // ------------- - write_pointer_.column = end_column; - write_cycles_pool -= write_cycles; - - if(write_pointer_.column == 342) { - write_pointer_.column = 0; - write_pointer_.row = (write_pointer_.row + 1) % mode_timing_.total_lines; - LineBuffer &next_line_buffer = line_buffers_[write_pointer_.row]; - - // Establish the output mode for the next line. - set_current_screen_mode(); - - // Based on the output mode, pick a line mode. - next_line_buffer.first_pixel_output_column = 86; - next_line_buffer.next_border_column = 342; - mode_timing_.maximum_visible_sprites = 4; - switch(screen_mode_) { - case ScreenMode::Text: - next_line_buffer.line_mode = LineMode::Text; - next_line_buffer.first_pixel_output_column = 94; - next_line_buffer.next_border_column = 334; - break; - case ScreenMode::SMSMode4: - next_line_buffer.line_mode = LineMode::SMS; - mode_timing_.maximum_visible_sprites = 8; - break; - default: - next_line_buffer.line_mode = LineMode::Character; - break; - } - - if( - (screen_mode_ == ScreenMode::Blank) || - (write_pointer_.row >= mode_timing_.pixel_lines && write_pointer_.row != mode_timing_.total_lines-1)) - next_line_buffer.line_mode = LineMode::Refresh; - } - } - - -#ifndef NDEBUG - assert(backup.row == read_pointer_.row && backup.column == read_pointer_.column); - backup = write_pointer_; -#endif - - - if(read_cycles_pool) { - // Determine how much time has passed in the remainder of this line, and proceed. - const int target_read_cycles = std::min(342 - read_pointer_.column, read_cycles_pool); - int read_cycles_performed = 0; - uint32_t next_cram_value = 0; - - while(read_cycles_performed < target_read_cycles) { - const uint32_t cram_value = next_cram_value; - next_cram_value = 0; - int read_cycles = target_read_cycles - read_cycles_performed; - if(!upcoming_cram_dots_.empty() && upcoming_cram_dots_.front().location.row == read_pointer_.row) { - int time_until_dot = upcoming_cram_dots_.front().location.column - read_pointer_.column; - - if(time_until_dot < read_cycles) { - read_cycles = time_until_dot; - next_cram_value = upcoming_cram_dots_.front().value; - upcoming_cram_dots_.erase(upcoming_cram_dots_.begin()); - } - } - - if(!read_cycles) continue; - read_cycles_performed += read_cycles; - - const int end_column = read_pointer_.column + read_cycles; - LineBuffer &line_buffer = line_buffers_[read_pointer_.row]; - - - // -------------------- - // Output video stream. - // -------------------- - -#define intersect(left, right, code) { \ - const int start = std::max(read_pointer_.column, left); \ - const int end = std::min(end_column, right); \ - if(end > start) {\ - code;\ - }\ - } - -#define border(left, right) intersect(left, right, output_border(end - start, cram_value)) - - if(line_buffer.line_mode == LineMode::Refresh || read_pointer_.row > mode_timing_.pixel_lines) { - if(read_pointer_.row >= mode_timing_.first_vsync_line && read_pointer_.row < mode_timing_.first_vsync_line+4) { - // Vertical sync. - if(end_column == 342) { - crt_.output_sync(342 * 4); - } - } else { - // Right border. - border(0, 15); - - // Blanking region; total length is 58 cycles, - // and 58+15 = 73. So output the lot when the - // cursor passes 73. - if(read_pointer_.column < 73 && end_column >= 73) { - crt_.output_blank(8*4); - crt_.output_sync(26*4); - crt_.output_blank(2*4); - crt_.output_default_colour_burst(14*4); - crt_.output_blank(8*4); - } - - // Border colour for the rest of the line. - border(73, 342); - } - } else { - // Right border. - border(0, 15); - - // Blanking region. - if(read_pointer_.column < 73 && end_column >= 73) { - crt_.output_blank(8*4); - crt_.output_sync(26*4); - crt_.output_blank(2*4); - crt_.output_default_colour_burst(14*4); - crt_.output_blank(8*4); - } - - // Left border. - border(73, line_buffer.first_pixel_output_column); - - // Pixel region. - intersect( - line_buffer.first_pixel_output_column, - line_buffer.next_border_column, - if(!asked_for_write_area_) { - asked_for_write_area_ = true; - pixel_origin_ = pixel_target_ = reinterpret_cast( - crt_.begin_data(size_t(line_buffer.next_border_column - line_buffer.first_pixel_output_column)) - ); - } - - if(pixel_target_) { - const int relative_start = start - line_buffer.first_pixel_output_column; - const int relative_end = end - line_buffer.first_pixel_output_column; - switch(line_buffer.line_mode) { - case LineMode::SMS: draw_sms(relative_start, relative_end, cram_value); break; - case LineMode::Character: draw_tms_character(relative_start, relative_end); break; - case LineMode::Text: draw_tms_text(relative_start, relative_end); break; - - case LineMode::Refresh: break; /* Dealt with elsewhere. */ - } - } - - if(end == line_buffer.next_border_column) { - const int length = line_buffer.next_border_column - line_buffer.first_pixel_output_column; - crt_.output_data(length * 4, size_t(length)); - pixel_origin_ = pixel_target_ = nullptr; - asked_for_write_area_ = false; - } - ); - - // Additional right border, if called for. - if(line_buffer.next_border_column != 342) { - border(line_buffer.next_border_column, 342); - } - } - -#undef border -#undef intersect - - - - // ------------- - // Advance time. - // ------------- - read_pointer_.column = end_column; - } - - read_cycles_pool -= target_read_cycles; - if(read_pointer_.column == 342) { - read_pointer_.column = 0; - read_pointer_.row = (read_pointer_.row + 1) % mode_timing_.total_lines; - } - } - - assert(backup.row == write_pointer_.row && backup.column == write_pointer_.column); - } -} - -void Base::output_border(int cycles, uint32_t cram_dot) { - cycles *= 4; - uint32_t border_colour = - is_sega_vdp(personality_) ? - master_system_.colour_ram[16 + background_colour_] : - palette[background_colour_]; - - if(cram_dot) { - uint32_t *const pixel_target = reinterpret_cast(crt_.begin_data(1)); - if(pixel_target) { - *pixel_target = border_colour | cram_dot; - } - crt_.output_level(4); - cycles -= 4; - } - - if(cycles) { - // If the border colour is 0, that can be communicated - // more efficiently as an explicit blank. - if(border_colour) { - uint32_t *const pixel_target = reinterpret_cast(crt_.begin_data(1)); - if(pixel_target) { - *pixel_target = border_colour; - } - crt_.output_level(cycles); - } else { - crt_.output_blank(cycles); - } - } -} - -void TMS9918::write(int address, uint8_t value) { - // Writes to address 0 are writes to the video RAM. Store - // the value and return. - if(!(address & 1)) { - write_phase_ = false; - - // Enqueue the write to occur at the next available slot. - read_ahead_buffer_ = value; - queued_access_ = MemoryAccess::Write; - cycles_until_access_ = vram_access_delay(); - - return; - } - - // Writes to address 1 are performed in pairs; if this is the - // low byte of a value, store it and wait for the high byte. - if(!write_phase_) { - low_write_ = value; - write_phase_ = true; - - // The initial write should half update the access pointer. - ram_pointer_ = (ram_pointer_ & 0xff00) | low_write_; - return; - } - - // The RAM pointer is always set on a second write, regardless of - // whether the caller is intending to enqueue a VDP operation. - ram_pointer_ = (ram_pointer_ & 0x00ff) | uint16_t(value << 8); - - write_phase_ = false; - if(value & 0x80) { - if(is_sega_vdp(personality_)) { - if(value & 0x40) { - master_system_.cram_is_selected = true; - return; - } - value &= 0xf; - } else { - value &= 0x7; - } - - // This is a write to a register. - switch(value) { - case 0: - if(is_sega_vdp(personality_)) { - master_system_.vertical_scroll_lock = !!(low_write_ & 0x80); - master_system_.horizontal_scroll_lock = !!(low_write_ & 0x40); - master_system_.hide_left_column = !!(low_write_ & 0x20); - enable_line_interrupts_ = !!(low_write_ & 0x10); - master_system_.shift_sprites_8px_left = !!(low_write_ & 0x08); - master_system_.mode4_enable = !!(low_write_ & 0x04); - } - mode2_enable_ = !!(low_write_ & 0x02); - break; - - case 1: - blank_display_ = !(low_write_ & 0x40); - generate_interrupts_ = !!(low_write_ & 0x20); - mode1_enable_ = !!(low_write_ & 0x10); - mode3_enable_ = !!(low_write_ & 0x08); - sprites_16x16_ = !!(low_write_ & 0x02); - sprites_magnified_ = !!(low_write_ & 0x01); - - sprite_height_ = 8; - if(sprites_16x16_) sprite_height_ <<= 1; - if(sprites_magnified_) sprite_height_ <<= 1; - break; - - case 2: - pattern_name_address_ = size_t((low_write_ & 0xf) << 10) | 0x3ff; - master_system_.pattern_name_address = pattern_name_address_ | ((personality_ == TMS::SMSVDP) ? 0x000 : 0x400); - break; - - case 3: - colour_table_address_ = size_t(low_write_ << 6) | 0x3f; - break; - - case 4: - pattern_generator_table_address_ = size_t((low_write_ & 0x07) << 11) | 0x7ff; - break; - - case 5: - sprite_attribute_table_address_ = size_t((low_write_ & 0x7f) << 7) | 0x7f; - master_system_.sprite_attribute_table_address = sprite_attribute_table_address_ | ((personality_ == TMS::SMSVDP) ? 0x00 : 0x80); - break; - - case 6: - sprite_generator_table_address_ = size_t((low_write_ & 0x07) << 11) | 0x7ff; - master_system_.sprite_generator_table_address = sprite_generator_table_address_ | ((personality_ == TMS::SMSVDP) ? 0x0000 : 0x1800); - break; - - case 7: - text_colour_ = low_write_ >> 4; - background_colour_ = low_write_ & 0xf; - break; - - case 8: - if(is_sega_vdp(personality_)) { - master_system_.horizontal_scroll = low_write_; - } - break; - - case 9: - if(is_sega_vdp(personality_)) { - master_system_.vertical_scroll = low_write_; - } - break; - - case 10: - if(is_sega_vdp(personality_)) { - line_interrupt_target = low_write_; - } - break; - - default: - LOG("Unknown TMS write: " << int(low_write_) << " to " << int(value)); - break; - } - } else { - // This is an access via the RAM pointer. - if(!(value & 0x40)) { - // A read request is enqueued upon setting the address; conversely a write - // won't be enqueued unless and until some actual data is supplied. - queued_access_ = MemoryAccess::Read; - cycles_until_access_ = vram_access_delay(); - } - master_system_.cram_is_selected = false; - } -} - -uint8_t TMS9918::get_current_line() { - // Determine the row to return. - constexpr int row_change_position = 63; // This is the proper Master System value; substitute if any other VDPs turn out to have this functionality. - int source_row = - (write_pointer_.column < row_change_position) - ? (write_pointer_.row + mode_timing_.total_lines - 1)%mode_timing_.total_lines - : write_pointer_.row; - - if(tv_standard_ == TVStandard::NTSC) { - if(mode_timing_.pixel_lines == 240) { - // NTSC 256x240: 00-FF, 00-06 - } else if(mode_timing_.pixel_lines == 224) { - // NTSC 256x224: 00-EA, E5-FF - if(source_row >= 0xeb) source_row -= 6; - } else { - // NTSC 256x192: 00-DA, D5-FF - if(source_row >= 0xdb) source_row -= 6; - } - } else { - if(mode_timing_.pixel_lines == 240) { - // PAL 256x240: 00-FF, 00-0A, D2-FF - if(source_row >= 267) source_row -= 0x39; - } else if(mode_timing_.pixel_lines == 224) { - // PAL 256x224: 00-FF, 00-02, CA-FF - if(source_row >= 259) source_row -= 0x39; - } else { - // PAL 256x192: 00-F2, BA-FF - if(source_row >= 0xf3) source_row -= 0x39; - } - } - - return uint8_t(source_row); -} - -uint8_t TMS9918::get_latched_horizontal_counter() { - // Translate from internal numbering, which puts pixel output - // in the final 256 pixels of 342, to the public numbering, - // which makes the 256 pixels the first 256 spots, but starts - // counting at -48, and returns only the top 8 bits of the number. - int public_counter = latched_column_ - 86; - if(public_counter < -46) public_counter += 342; - return uint8_t(public_counter >> 1); -} - -void TMS9918::latch_horizontal_counter() { - latched_column_ = write_pointer_.column; -} - -uint8_t TMS9918::read(int address) { - write_phase_ = false; - - // Reads from address 0 read video RAM, via the read-ahead buffer. - if(!(address & 1)) { - // Enqueue the write to occur at the next available slot. - uint8_t result = read_ahead_buffer_; - queued_access_ = MemoryAccess::Read; - return result; - } - - // Reads from address 1 get the status register. - uint8_t result = status_; - status_ &= ~(StatusInterrupt | StatusSpriteOverflow | StatusSpriteCollision); - line_interrupt_pending_ = false; - return result; -} - -HalfCycles Base::half_cycles_before_internal_cycles(int internal_cycles) { - return HalfCycles(((internal_cycles << 2) + (2 - cycles_error_)) / 3); -} - -HalfCycles TMS9918::get_next_sequence_point() { - if(!generate_interrupts_ && !enable_line_interrupts_) return HalfCycles::max(); - if(get_interrupt_line()) return HalfCycles::max(); - - // Calculate the amount of time until the next end-of-frame interrupt. - const int frame_length = 342 * mode_timing_.total_lines; - int time_until_frame_interrupt = - ( - ((mode_timing_.end_of_frame_interrupt_position.row * 342) + mode_timing_.end_of_frame_interrupt_position.column + frame_length) - - ((write_pointer_.row * 342) + write_pointer_.column) - ) % frame_length; - if(!time_until_frame_interrupt) time_until_frame_interrupt = frame_length; - - if(!enable_line_interrupts_) return half_cycles_before_internal_cycles(time_until_frame_interrupt); - - // Calculate when the next line interrupt will occur. - int next_line_interrupt_row = -1; - - int cycles_to_next_interrupt_threshold = mode_timing_.line_interrupt_position - write_pointer_.column; - int line_of_next_interrupt_threshold = write_pointer_.row; - if(cycles_to_next_interrupt_threshold <= 0) { - cycles_to_next_interrupt_threshold += 342; - ++line_of_next_interrupt_threshold; - } - - if(is_sega_vdp(personality_)) { - // If there is still time for a line interrupt this frame, that'll be it; - // otherwise it'll be on the next frame, supposing there's ever time for - // it at all. - if(line_of_next_interrupt_threshold + line_interrupt_counter <= mode_timing_.pixel_lines) { - next_line_interrupt_row = line_of_next_interrupt_threshold + line_interrupt_counter; - } else { - if(line_interrupt_target <= mode_timing_.pixel_lines) - next_line_interrupt_row = mode_timing_.total_lines + line_interrupt_target; - } - } - - // If there's actually no interrupt upcoming, despite being enabled, either return - // the frame end interrupt or no interrupt pending as appropriate. - if(next_line_interrupt_row == -1) { - return generate_interrupts_ ? - half_cycles_before_internal_cycles(time_until_frame_interrupt) : - HalfCycles::max(); - } - - // Figure out the number of internal cycles until the next line interrupt, which is the amount - // of time to the next tick over and then next_line_interrupt_row - row_ lines further. - const int local_cycles_until_line_interrupt = cycles_to_next_interrupt_threshold + (next_line_interrupt_row - line_of_next_interrupt_threshold) * 342; - if(!generate_interrupts_) return half_cycles_before_internal_cycles(local_cycles_until_line_interrupt); - - // Return whichever interrupt is closer. - return half_cycles_before_internal_cycles(std::min(local_cycles_until_line_interrupt, time_until_frame_interrupt)); -} - -HalfCycles TMS9918::get_time_until_line(int line) { - if(line < 0) line += mode_timing_.total_lines; - - int cycles_to_next_interrupt_threshold = mode_timing_.line_interrupt_position - write_pointer_.column; - int line_of_next_interrupt_threshold = write_pointer_.row; - if(cycles_to_next_interrupt_threshold <= 0) { - cycles_to_next_interrupt_threshold += 342; - ++line_of_next_interrupt_threshold; - } - - if(line_of_next_interrupt_threshold > line) { - line += mode_timing_.total_lines; - } - - return half_cycles_before_internal_cycles(cycles_to_next_interrupt_threshold + (line - line_of_next_interrupt_threshold)*342); -} - -bool TMS9918::get_interrupt_line() { - return ((status_ & StatusInterrupt) && generate_interrupts_) || (enable_line_interrupts_ && line_interrupt_pending_); -} - -// MARK: - - -void Base::draw_tms_character(int start, int end) { - LineBuffer &line_buffer = line_buffers_[read_pointer_.row]; - - // Paint the background tiles. - const int pixels_left = end - start; - if(screen_mode_ == ScreenMode::MultiColour) { - for(int c = start; c < end; ++c) { - pixel_target_[c] = palette[ - (line_buffer.patterns[c >> 3][0] >> (((c & 4)^4))) & 15 - ]; - } - } else { - const int shift = start & 7; - int byte_column = start >> 3; - - int length = std::min(pixels_left, 8 - shift); - - int pattern = reverse_table.map[line_buffer.patterns[byte_column][0]] >> shift; - uint8_t colour = line_buffer.patterns[byte_column][1]; - uint32_t colours[2] = { - palette[(colour & 15) ? (colour & 15) : background_colour_], - palette[(colour >> 4) ? (colour >> 4) : background_colour_] - }; - - int background_pixels_left = pixels_left; - while(true) { - background_pixels_left -= length; - for(int c = 0; c < length; ++c) { - pixel_target_[c] = colours[pattern&0x01]; - pattern >>= 1; - } - pixel_target_ += length; - - if(!background_pixels_left) break; - length = std::min(8, background_pixels_left); - byte_column++; - - pattern = reverse_table.map[line_buffer.patterns[byte_column][0]]; - colour = line_buffer.patterns[byte_column][1]; - colours[0] = palette[(colour & 15) ? (colour & 15) : background_colour_]; - colours[1] = palette[(colour >> 4) ? (colour >> 4) : background_colour_]; - } - } - - // Paint sprites and check for collisions, but only if at least one sprite is active - // on this line. - if(line_buffer.active_sprite_slot) { - const int shift_advance = sprites_magnified_ ? 1 : 2; - // If this is the start of the line clip any part of any sprites that is off to the left. - if(!start) { - for(int index = 0; index < line_buffer.active_sprite_slot; ++index) { - LineBuffer::ActiveSprite &sprite = line_buffer.active_sprites[index]; - if(sprite.x < 0) sprite.shift_position -= shift_advance * sprite.x; - } - } - - int sprite_buffer[256]; - int sprite_collision = 0; - memset(&sprite_buffer[start], 0, size_t(end - start)*sizeof(sprite_buffer[0])); - - constexpr uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff}; - constexpr int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; - - // Draw all sprites into the sprite buffer. - const int shifter_target = sprites_16x16_ ? 32 : 16; - for(int index = line_buffer.active_sprite_slot - 1; index >= 0; --index) { - LineBuffer::ActiveSprite &sprite = line_buffer.active_sprites[index]; - if(sprite.shift_position < shifter_target) { - const int pixel_start = std::max(start, sprite.x); - for(int c = pixel_start; c < end && sprite.shift_position < shifter_target; ++c) { - const int shift = (sprite.shift_position >> 1) ^ 7; - int sprite_colour = (sprite.image[shift >> 3] >> (shift & 7)) & 1; - - // A colision is detected regardless of sprite colour ... - sprite_collision |= sprite_buffer[c] & sprite_colour; - sprite_buffer[c] |= sprite_colour; - - // ... but a sprite with the transparent colour won't actually be visible. - sprite_colour &= colour_masks[sprite.image[2]&15]; - pixel_origin_[c] = - (pixel_origin_[c] & sprite_colour_selection_masks[sprite_colour^1]) | - (palette[sprite.image[2]&15] & sprite_colour_selection_masks[sprite_colour]); - - sprite.shift_position += shift_advance; - } - } - } - - status_ |= sprite_collision << StatusSpriteCollisionShift; - } -} - -void Base::draw_tms_text(int start, int end) { - LineBuffer &line_buffer = line_buffers_[read_pointer_.row]; - const uint32_t colours[2] = { palette[background_colour_], palette[text_colour_] }; - - const int shift = start % 6; - int byte_column = start / 6; - int pattern = reverse_table.map[line_buffer.patterns[byte_column][0]] >> shift; - int pixels_left = end - start; - int length = std::min(pixels_left, 6 - shift); - while(true) { - pixels_left -= length; - for(int c = 0; c < length; ++c) { - pixel_target_[c] = colours[pattern&0x01]; - pattern >>= 1; - } - pixel_target_ += length; - - if(!pixels_left) break; - length = std::min(6, pixels_left); - byte_column++; - pattern = reverse_table.map[line_buffer.patterns[byte_column][0]]; - } -} - -void Base::draw_sms(int start, int end, uint32_t cram_dot) { - LineBuffer &line_buffer = line_buffers_[read_pointer_.row]; - int colour_buffer[256]; - - /* - Add extra border for any pixels that fall before the fine scroll. - */ - int tile_start = start, tile_end = end; - int tile_offset = start; - if(read_pointer_.row >= 16 || !master_system_.horizontal_scroll_lock) { - for(int c = start; c < (line_buffer.latched_horizontal_scroll & 7); ++c) { - colour_buffer[c] = 16 + background_colour_; - ++tile_offset; - } - - // Remove the border area from that to which tiles will be drawn. - tile_start = std::max(start - (line_buffer.latched_horizontal_scroll & 7), 0); - tile_end = std::max(end - (line_buffer.latched_horizontal_scroll & 7), 0); - } - - - uint32_t pattern; - uint8_t *const pattern_index = reinterpret_cast(&pattern); - - /* - Add background tiles; these will fill the colour_buffer with values in which - the low five bits are a palette index, and bit six is set if this tile has - priority over sprites. - */ - if(tile_start < end) { - const int shift = tile_start & 7; - int byte_column = tile_start >> 3; - int pixels_left = tile_end - tile_start; - int length = std::min(pixels_left, 8 - shift); - - pattern = *reinterpret_cast(line_buffer.patterns[byte_column]); - if(line_buffer.names[byte_column].flags&2) - pattern >>= shift; - else - pattern <<= shift; - - while(true) { - const int palette_offset = (line_buffer.names[byte_column].flags&0x18) << 1; - if(line_buffer.names[byte_column].flags&2) { - for(int c = 0; c < length; ++c) { - colour_buffer[tile_offset] = - ((pattern_index[3] & 0x01) << 3) | - ((pattern_index[2] & 0x01) << 2) | - ((pattern_index[1] & 0x01) << 1) | - ((pattern_index[0] & 0x01) << 0) | - palette_offset; - ++tile_offset; - pattern >>= 1; - } - } else { - for(int c = 0; c < length; ++c) { - colour_buffer[tile_offset] = - ((pattern_index[3] & 0x80) >> 4) | - ((pattern_index[2] & 0x80) >> 5) | - ((pattern_index[1] & 0x80) >> 6) | - ((pattern_index[0] & 0x80) >> 7) | - palette_offset; - ++tile_offset; - pattern <<= 1; - } - } - - pixels_left -= length; - if(!pixels_left) break; - - length = std::min(8, pixels_left); - byte_column++; - pattern = *reinterpret_cast(line_buffer.patterns[byte_column]); - } - } - - /* - Apply sprites (if any). - */ - if(line_buffer.active_sprite_slot) { - const int shift_advance = sprites_magnified_ ? 1 : 2; - - // If this is the start of the line clip any part of any sprites that is off to the left. - if(!start) { - for(int index = 0; index < line_buffer.active_sprite_slot; ++index) { - LineBuffer::ActiveSprite &sprite = line_buffer.active_sprites[index]; - if(sprite.x < 0) sprite.shift_position -= shift_advance * sprite.x; - } - } - - int sprite_buffer[256]; - int sprite_collision = 0; - memset(&sprite_buffer[start], 0, size_t(end - start)*sizeof(sprite_buffer[0])); - - // Draw all sprites into the sprite buffer. - for(int index = line_buffer.active_sprite_slot - 1; index >= 0; --index) { - LineBuffer::ActiveSprite &sprite = line_buffer.active_sprites[index]; - if(sprite.shift_position < 16) { - const int pixel_start = std::max(start, sprite.x); - - // TODO: it feels like the work below should be simplifiable; - // the double shift in particular, and hopefully the variable shift. - for(int c = pixel_start; c < end && sprite.shift_position < 16; ++c) { - const int shift = (sprite.shift_position >> 1); - const int sprite_colour = - (((sprite.image[3] << shift) & 0x80) >> 4) | - (((sprite.image[2] << shift) & 0x80) >> 5) | - (((sprite.image[1] << shift) & 0x80) >> 6) | - (((sprite.image[0] << shift) & 0x80) >> 7); - - if(sprite_colour) { - sprite_collision |= sprite_buffer[c]; - sprite_buffer[c] = sprite_colour | 0x10; - } - - sprite.shift_position += shift_advance; - } - } - } - - // Draw the sprite buffer onto the colour buffer, wherever the tile map doesn't have - // priority (or is transparent). - for(int c = start; c < end; ++c) { - if( - sprite_buffer[c] && - (!(colour_buffer[c]&0x20) || !(colour_buffer[c]&0xf)) - ) colour_buffer[c] = sprite_buffer[c]; - } - - if(sprite_collision) - status_ |= StatusSpriteCollision; - } - - // Map from the 32-colour buffer to real output pixels, applying the specific CRAM dot if any. - pixel_target_[start] = master_system_.colour_ram[colour_buffer[start] & 0x1f] | cram_dot; - for(int c = start+1; c < end; ++c) { - pixel_target_[c] = master_system_.colour_ram[colour_buffer[c] & 0x1f]; - } - - // If the VDP is set to hide the left column and this is the final call that'll come - // this line, hide it. - if(end == 256) { - if(master_system_.hide_left_column) { - pixel_origin_[0] = pixel_origin_[1] = pixel_origin_[2] = pixel_origin_[3] = - pixel_origin_[4] = pixel_origin_[5] = pixel_origin_[6] = pixel_origin_[7] = - master_system_.colour_ram[16 + background_colour_]; - } - } -} diff --git a/Components/9918/9918.hpp b/Components/9918/9918.hpp index 806977e31..a8ae753e7 100644 --- a/Components/9918/9918.hpp +++ b/Components/9918/9918.hpp @@ -6,18 +6,41 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef TMS9918_hpp -#define TMS9918_hpp +#pragma once #include "../../Outputs/CRT/CRT.hpp" #include "../../ClockReceiver/ClockReceiver.hpp" -#include "Implementation/9918Base.hpp" - #include -namespace TI { -namespace TMS { +namespace TI::TMS { + +enum Personality { + TMS9918A, // includes the 9928 and 9929; set TV standard and output device as desired. + + // Yamaha extensions. + V9938, + V9958, + + // Sega extensions. + SMSVDP, + SMS2VDP, + GGVDP, + MDVDP, +}; + +enum class TVStandard { + /*! i.e. 50Hz output at around 312.5 lines/field */ + PAL, + /*! i.e. 60Hz output at around 262.5 lines/field */ + NTSC +}; + +} + +#include "Implementation/9918Base.hpp" + +namespace TI::TMS { /*! Provides emulation of the TMS9918a, TMS9928 and TMS9929. Likely in the future to be the @@ -30,13 +53,10 @@ namespace TMS { These chips have only one non-on-demand interaction with the outside world: an interrupt line. See get_time_until_interrupt and get_interrupt_line for asynchronous operation options. */ -class TMS9918: public Base { +template class TMS9918: private Base { public: - /*! - Constructs an instance of the drive controller that behaves according to personality @c p. - @param p The type of controller to emulate. - */ - TMS9918(Personality p); + /*! Constructs an instance of the VDP that behaves according to the templated personality. */ + TMS9918(); /*! Sets the TV standard for this TMS, if that is hard-coded in hardware. */ void set_tv_standard(TVStandard standard); @@ -44,18 +64,24 @@ class TMS9918: public Base { /*! Sets the scan target this TMS will post content to. */ void set_scan_target(Outputs::Display::ScanTarget *); - /// Gets the current scan status. + /*! Gets the current scan status. */ Outputs::Display::ScanStatus get_scaled_scan_status() const; - /*! Sets the type of display the CRT will request. */ + /*! Sets the type of CRT display. */ void set_display_type(Outputs::Display::DisplayType); - /*! Gets the type of display the CRT will request. */ + /*! Gets the type of CRT display. */ Outputs::Display::DisplayType get_display_type() const; /*! - Runs the VCP for the number of cycles indicate; it is an implicit assumption of the code - that the input clock rate is 3579545 Hz, the NTSC colour clock rate. + Runs the VDP for the number of cycles indicate; the input clock rate is implicitly assumed. + + For everything except the Mega Drive VDP: + * the input clock rate should be 3579545 Hz, the NTSC colour clock rate. + + For the Mega Drive: + * the input clock rate should be around 7.6MHz; 15/7ths of the NTSC colour + clock rate for NTSC output and 12/7ths of the PAL colour clock rate for PAL output. */ void run_for(const HalfCycles cycles); @@ -65,11 +91,11 @@ class TMS9918: public Base { /*! Gets a register value. */ uint8_t read(int address); - /*! Gets the current scan line; provided by the Master System only. */ - uint8_t get_current_line(); + /*! Gets the current scan line; provided by the Sega VDPs only. */ + uint8_t get_current_line() const; - /*! Gets the current latched horizontal counter; provided by the Master System only. */ - uint8_t get_latched_horizontal_counter(); + /*! Gets the current latched horizontal counter; provided by the Sega VDPs only. */ + uint8_t get_latched_horizontal_counter() const; /*! Latches the current horizontal counter. */ void latch_horizontal_counter(); @@ -81,7 +107,7 @@ class TMS9918: public Base { If get_interrupt_line is true now of if get_interrupt_line would never return true, returns HalfCycles::max(). */ - HalfCycles get_next_sequence_point(); + HalfCycles next_sequence_point() const; /*! Returns the amount of time until the nominated line interrupt position is @@ -96,10 +122,7 @@ class TMS9918: public Base { /*! @returns @c true if the interrupt line is currently active; @c false otherwise. */ - bool get_interrupt_line(); + bool get_interrupt_line() const; }; } -} - -#endif /* TMS9918_hpp */ diff --git a/Components/9918/Implementation/9918.cpp b/Components/9918/Implementation/9918.cpp new file mode 100644 index 000000000..86f7639b9 --- /dev/null +++ b/Components/9918/Implementation/9918.cpp @@ -0,0 +1,1371 @@ +// +// 9918.cpp +// Clock Signal +// +// Created by Thomas Harte on 25/11/2017. +// Copyright 2017 Thomas Harte. All rights reserved. +// + +#include "../9918.hpp" + +#include +#include +#include +#include "../../../Outputs/Log.hpp" + +using namespace TI::TMS; + +namespace { + +// 342 internal cycles are 228/227.5ths of a line, so 341.25 cycles should be a whole +// line. Therefore multiply everything by four, but set line length to 1365 rather than 342*4 = 1368. +constexpr unsigned int CRTCyclesPerLine = 1365; +constexpr unsigned int CRTCyclesDivider = 4; + +Log::Logger logger; + +} + +template +Base::Base() : + crt_(CRTCyclesPerLine, CRTCyclesDivider, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Red8Green8Blue8) { + // Unimaginatively, this class just passes RGB through to the shader. Investigation is needed + // into whether there's a more natural form. It feels unlikely given the diversity of chips modelled. + + if constexpr (is_sega_vdp(personality)) { + // Cf. https://www.smspower.org/forums/8161-SMSDisplayTiming + + // "For a line interrupt, /INT is pulled low 608 mclks into the appropriate scanline relative to pixel 0. + // This is 3 mclks before the rising edge of /HSYNC which starts the next scanline." + // + // i.e. it's 304 internal clocks after the end of the left border. + mode_timing_.line_interrupt_position = (LineLayout::EndOfLeftBorder + 304) % LineLayout::CyclesPerLine; + + // For a frame interrupt, /INT is pulled low 607 mclks into scanline 192 (of scanlines 0 through 261) relative to pixel 0. + // This is 4 mclks before the rising edge of /HSYNC which starts the next scanline. + // + // i.e. it's 1/2 cycle before the line interrupt position, which I have rounded. Ugh. + mode_timing_.end_of_frame_interrupt_position.column = mode_timing_.line_interrupt_position - 1; + mode_timing_.end_of_frame_interrupt_position.row = 192 + (LineLayout::EndOfLeftBorder + 304) / LineLayout::CyclesPerLine; + } + + if constexpr (is_yamaha_vdp(personality)) { + // TODO: this is used for interrupt _prediction_ but won't handle text modes correctly, and indeed + // can't be just a single value where the programmer has changed into or out of text modes during the + // middle of a line, since screen mode is latched (so it'll be one value for that line, another from then onwards).a + mode_timing_.line_interrupt_position = LineLayout::EndOfPixels; + } + + // Establish that output is delayed after reading by `output_lag` cycles, + // i.e. the fetch pointer is currently _ahead_ of the output pointer. + output_pointer_.row = output_pointer_.column = 0; + + fetch_pointer_ = output_pointer_; + fetch_pointer_.column += output_lag; + + fetch_line_buffer_ = line_buffers_.begin(); + draw_line_buffer_ = line_buffers_.begin(); + fetch_sprite_buffer_ = sprite_buffers_.begin(); +} + +template +TMS9918::TMS9918() { + this->crt_.set_display_type(Outputs::Display::DisplayType::RGB); + + if constexpr (is_yamaha_vdp(personality)) { + this->crt_.set_visible_area(Outputs::Display::Rect(0.07f, 0.065f, 0.875f, 0.875f)); + } else { + this->crt_.set_visible_area(Outputs::Display::Rect(0.07f, 0.0375f, 0.875f, 0.875f)); + } + + // The TMS remains in-phase with the NTSC colour clock; this is an empirical measurement + // intended to produce the correct relationship between the hard edges between pixels and + // the colour clock. It was eyeballed rather than derived from any knowledge of the TMS + // colour burst generator because I've yet to find any. + this->crt_.set_immediate_default_phase(0.85f); +} + +template +void TMS9918::set_tv_standard(TVStandard standard) { + // TODO: the Yamaha is programmable on this at runtime. + this->tv_standard_ = standard; + switch(standard) { + case TVStandard::PAL: + this->mode_timing_.total_lines = 313; + this->mode_timing_.first_vsync_line = 253; + this->crt_.set_new_display_type(CRTCyclesPerLine, Outputs::Display::Type::PAL50); + break; + default: + this->mode_timing_.total_lines = 262; + this->mode_timing_.first_vsync_line = 227; + this->crt_.set_new_display_type(CRTCyclesPerLine, Outputs::Display::Type::NTSC60); + break; + } +} + +template +void TMS9918::set_scan_target(Outputs::Display::ScanTarget *scan_target) { + this->crt_.set_scan_target(scan_target); +} + +template +Outputs::Display::ScanStatus TMS9918::get_scaled_scan_status() const { + // The input was scaled by 3/4 to convert half cycles to internal ticks, + // so undo that and also allow for: (i) the multiply by 4 that it takes + // to reach the CRT; and (ii) the fact that the half-cycles value was scaled, + // and this should really reply in whole cycles. + return this->crt_.get_scaled_scan_status() * (4.0f / (3.0f * 8.0f)); +} + +template +void TMS9918::set_display_type(Outputs::Display::DisplayType display_type) { + this->crt_.set_display_type(display_type); +} + +template +Outputs::Display::DisplayType TMS9918::get_display_type() const { + return this->crt_.get_display_type(); +} + +void SpriteBuffer::reset_sprite_collection() { + sprites_stopped = false; + active_sprite_slot = 0; + + for(int c = 0; c < 8; ++c) { + active_sprites[c].shift_position = 0; + } +} + +template +void Base::posit_sprite(int sprite_number, int sprite_position, uint8_t screen_row) { + // Evaluation of visibility of sprite 0 is always the first step in + // populating a sprite buffer; so use it to uncork a new one. + if(!sprite_number) { + advance(fetch_sprite_buffer_); + fetched_sprites_ = &*fetch_sprite_buffer_; + fetch_sprite_buffer_->reset_sprite_collection(); + fetch_sprite_buffer_->sprite_terminator = mode_timing_.sprite_terminator(fetch_line_buffer_->screen_mode); + + if constexpr (SpriteBuffer::test_is_filling) { + fetch_sprite_buffer_->is_filling = true; + } + } + + if(!(status_ & StatusSpriteOverflow)) { + status_ = uint8_t((status_ & ~0x1f) | (sprite_number & 0x1f)); + } + if(fetch_sprite_buffer_->sprites_stopped) return; + + // A sprite Y of 208 means "don't scan the list any further". + if(mode_timing_.allow_sprite_terminator && sprite_position == fetch_sprite_buffer_->sprite_terminator) { + fetch_sprite_buffer_->sprites_stopped = true; + return; + } + + const auto sprite_row = uint8_t(screen_row - sprite_position); + if(sprite_row < 0 || sprite_row >= sprite_height_) return; + + if(fetch_sprite_buffer_->active_sprite_slot == mode_timing_.maximum_visible_sprites) { + status_ |= StatusSpriteOverflow; + return; + } + + auto &sprite = fetch_sprite_buffer_->active_sprites[fetch_sprite_buffer_->active_sprite_slot]; + sprite.index = sprite_number; + sprite.row = sprite_row >> (sprites_magnified_ ? 1 : 0); + ++fetch_sprite_buffer_->active_sprite_slot; +} + +template +void TMS9918::run_for(const HalfCycles cycles) { + // As specific as I've been able to get: + // Scanline time is always 228 cycles. + // PAL output is 313 lines total. NTSC output is 262 lines total. + // Interrupt is signalled upon entering the lower border. + + // Convert 456 clocked half cycles per line to 342 internal cycles per line; + // the internal clock is 1.5 times the nominal 3.579545 Mhz that I've advertised + // for this part. So multiply by three quarters. + const int int_cycles = this->clock_converter_.to_internal(cycles.as()); + if(!int_cycles) return; + + // There are two intertwined processes here, 'fetching' (i.e. writing to the + // line buffers with newly-fetched video contents) and 'output' (reading from + // the line buffers and generating video). + int fetch_cycles_pool = int_cycles; + int output_cycles_pool = int_cycles; + + while(fetch_cycles_pool || output_cycles_pool) { +#ifndef NDEBUG + LineBufferPointer backup = this->output_pointer_; +#endif + + if(fetch_cycles_pool) { + // Determine how much writing to do; at the absolute most go to the end of this line. + const int fetch_cycles = std::min( + LineLayout::CyclesPerLine - this->fetch_pointer_.column, + fetch_cycles_pool + ); + const int end_column = this->fetch_pointer_.column + fetch_cycles; + + // ... and to any pending Yamaha commands. + if constexpr (is_yamaha_vdp(personality)) { + if(Storage::command_) { + Storage::minimum_command_column_ = + this->fetch_pointer_.column + Storage::command_->cycles; + Storage::command_->cycles -= fetch_cycles; + } + } + + + // --------------------------------------- + // Latch scrolling position, if necessary. + // --------------------------------------- + if constexpr (is_sega_vdp(personality)) { + // TODO: where did this magic constant come from? https://www.smspower.org/forums/17970-RoadRashHow#111000 mentioned in passing + // that "the vertical scroll register is latched at the start of the active display" and this is two clocks before that, so it's + // not uncompelling. I can just no longer find my source. + constexpr auto latch_time = LineLayout::EndOfLeftBorder - 2; + static_assert(latch_time > 0); + if(this->fetch_pointer_.column < latch_time && end_column >= latch_time) { + if(!this->fetch_pointer_.row) { + Storage::latched_vertical_scroll_ = Storage::vertical_scroll_; + + if(Storage::mode4_enable_) { + this->mode_timing_.pixel_lines = 192; + if(this->mode2_enable_ && this->mode1_enable_) this->mode_timing_.pixel_lines = 224; + if(this->mode2_enable_ && this->mode3_enable_) this->mode_timing_.pixel_lines = 240; + + this->mode_timing_.allow_sprite_terminator = this->mode_timing_.pixel_lines == 192; + this->mode_timing_.first_vsync_line = (this->mode_timing_.total_lines + this->mode_timing_.pixel_lines) >> 1; + + this->mode_timing_.end_of_frame_interrupt_position.row = this->mode_timing_.pixel_lines + 1; + } + } + this->fetch_line_buffer_->latched_horizontal_scroll = Storage::horizontal_scroll_; + } + } + + + // ------------------------ + // Perform memory accesses. + // ------------------------ +#define fetch(function, clock, offset) { \ + const int first_window = from_internal(this->fetch_pointer_.column); \ + const int final_window = from_internal(end_column); \ + if(first_window == final_window) break; \ + const auto y = uint8_t( \ + this->fetch_line_buffer_->vertical_state == VerticalState::Prefetch ? \ + offset - 1 : (this->fetch_pointer_.row + offset)); \ + if(final_window != clock_rate()) { \ + function(y, first_window, final_window); \ + } else { \ + function(y, first_window, final_window); \ + } \ +} + + + + if constexpr (is_yamaha_vdp(personality)) { + fetch(this->template fetch_yamaha, Clock::Internal, Storage::vertical_offset_); + } else { + switch(this->fetch_line_buffer_->fetch_mode) { + case FetchMode::Text: fetch(this->template fetch_tms_text, Clock::TMSMemoryWindow, 0); break; + case FetchMode::Character: fetch(this->template fetch_tms_character, Clock::TMSMemoryWindow, 0); break; + case FetchMode::SMS: fetch(this->template fetch_sms, Clock::TMSMemoryWindow, 0); break; + case FetchMode::Refresh: fetch(this->template fetch_tms_refresh, Clock::TMSMemoryWindow, 0); break; + + default: break; + } + } + +#undef fetch + + + // ------------------------------- + // Check for interrupt conditions. + // ------------------------------- + if constexpr (is_sega_vdp(personality)) { + // The Sega VDP offers a decrementing counter for triggering line interrupts; + // it is reloaded either when it overflows or upon every non-pixel line after the first. + // It is otherwise decremented. + if( + this->fetch_pointer_.column < this->mode_timing_.line_interrupt_position && + end_column >= this->mode_timing_.line_interrupt_position + ) { + if(this->fetch_pointer_.row >= 0 && this->fetch_pointer_.row <= this->mode_timing_.pixel_lines) { + if(!this->line_interrupt_counter_) { + this->line_interrupt_pending_ = true; + this->line_interrupt_counter_ = this->line_interrupt_target_; + } else { + --this->line_interrupt_counter_; + } + } else { + this->line_interrupt_counter_ = this->line_interrupt_target_; + } + } + } + + if constexpr (is_yamaha_vdp(personality)) { + // The Yamaha VDPs allow the user to specify which line an interrupt should occur on, + // which is relative to the current vertical base. Such an interrupt will occur immediately + // after pixels have ended. + if( + this->vertical_active_ && + this->fetch_pointer_.column < Storage::mode_description_.end_cycle && + end_column >= Storage::mode_description_.end_cycle && + this->fetch_pointer_.row == ((this->line_interrupt_target_ - Storage::vertical_offset_) & 0xff) + ) { + this->line_interrupt_pending_ = true; + Storage::line_matches_ = true; + } + + if( + this->fetch_pointer_.column < Storage::mode_description_.start_cycle && + end_column >= Storage::mode_description_.start_cycle + ) { + Storage::line_matches_ = false; + } + } + + if( + this->fetch_pointer_.row == this->mode_timing_.end_of_frame_interrupt_position.row && + this->fetch_pointer_.column < this->mode_timing_.end_of_frame_interrupt_position.column && + end_column >= this->mode_timing_.end_of_frame_interrupt_position.column + ) { + this->status_ |= StatusInterrupt; + } + + + + // ------------- + // Advance time. + // ------------- + this->fetch_pointer_.column = end_column; + fetch_cycles_pool -= fetch_cycles; + + // Check for end of line. + if(this->fetch_pointer_.column == LineLayout::CyclesPerLine) { + this->fetch_pointer_.column = 0; + this->fetch_pointer_.row = (this->fetch_pointer_.row + 1) % this->mode_timing_.total_lines; + + this->vertical_active_ |= !this->fetch_pointer_.row; + this->vertical_active_ &= this->fetch_pointer_.row != this->mode_timing_.pixel_lines; + + // Yamaha: handle blinking. + if constexpr (is_yamaha_vdp(personality)) { + if(!this->fetch_pointer_.row && Storage::blink_periods_) { + --Storage::blink_counter_; + while(!Storage::blink_counter_) { + Storage::in_blink_ ^= 1; + Storage::blink_counter_ = (Storage::blink_periods_ >> (Storage::in_blink_ << 2)) & 0xf; + } + } + } + + // Progress towards any delayed events. + this->minimum_access_column_ = + std::max( + 0, + this->minimum_access_column_ - LineLayout::CyclesPerLine + ); + if constexpr (is_yamaha_vdp(personality)) { + Storage::minimum_command_column_ = + std::max( + 0, + Storage::minimum_command_column_ - LineLayout::CyclesPerLine + ); + } + + this->advance(this->fetch_line_buffer_); + if(this->fetched_sprites_ && this->fetched_sprites_->active_sprite_slot) { + this->fetch_line_buffer_->sprites = this->fetched_sprites_; + this->fetched_sprites_ = nullptr; + } else { + this->fetch_line_buffer_->sprites = nullptr; + } + + // Establish the current screen output mode, which will be captured as a + // line mode momentarily. + this->screen_mode_ = this->template current_screen_mode(); + this->underlying_mode_ = this->template current_screen_mode(); + + if constexpr (is_yamaha_vdp(personality)) { + auto &desc = Storage::mode_description_; + desc.pixels_per_byte = pixels_per_byte(this->underlying_mode_); + desc.width = width(this->underlying_mode_); + desc.rotate_address = interleaves_banks(this->underlying_mode_); + if(is_text(this->underlying_mode_)) { + desc.start_cycle = LineLayout::TextModeEndOfLeftBorder; + desc.end_cycle = LineLayout::TextModeEndOfPixels; + } else { + desc.start_cycle = LineLayout::EndOfLeftBorder; + desc.end_cycle = LineLayout::EndOfPixels; + } + } + + // Based on the output mode, pick a line mode. + this->fetch_line_buffer_->first_pixel_output_column = LineLayout::EndOfLeftBorder; + this->fetch_line_buffer_->next_border_column = LineLayout::EndOfPixels; + this->fetch_line_buffer_->pixel_count = 256; + this->fetch_line_buffer_->screen_mode = this->screen_mode_; + this->mode_timing_.maximum_visible_sprites = 4; + switch(this->screen_mode_) { + case ScreenMode::Text: + if constexpr (is_yamaha_vdp(personality)) { + this->fetch_line_buffer_->fetch_mode = FetchMode::Yamaha; + } else { + this->fetch_line_buffer_->fetch_mode = FetchMode::Text; + } + this->fetch_line_buffer_->first_pixel_output_column = LineLayout::TextModeEndOfLeftBorder; + this->fetch_line_buffer_->next_border_column = LineLayout::TextModeEndOfPixels; + this->fetch_line_buffer_->pixel_count = 240; + break; + case ScreenMode::YamahaText80: + this->fetch_line_buffer_->fetch_mode = FetchMode::Yamaha; + this->fetch_line_buffer_->first_pixel_output_column = LineLayout::TextModeEndOfLeftBorder; + this->fetch_line_buffer_->next_border_column = LineLayout::TextModeEndOfPixels; + this->fetch_line_buffer_->pixel_count = 480; + break; + + case ScreenMode::SMSMode4: + this->fetch_line_buffer_->fetch_mode = FetchMode::SMS; + this->mode_timing_.maximum_visible_sprites = 8; + break; + + case ScreenMode::YamahaGraphics3: + case ScreenMode::YamahaGraphics4: + case ScreenMode::YamahaGraphics7: + this->fetch_line_buffer_->fetch_mode = FetchMode::Yamaha; + this->mode_timing_.maximum_visible_sprites = 8; + break; + case ScreenMode::YamahaGraphics5: + case ScreenMode::YamahaGraphics6: + this->fetch_line_buffer_->pixel_count = 512; + this->fetch_line_buffer_->fetch_mode = FetchMode::Yamaha; + this->mode_timing_.maximum_visible_sprites = 8; + break; + default: + // This covers both MultiColour and Graphics modes. + if constexpr (is_yamaha_vdp(personality)) { + this->fetch_line_buffer_->fetch_mode = FetchMode::Yamaha; + } else { + this->fetch_line_buffer_->fetch_mode = FetchMode::Character; + } + break; + } + + if constexpr (is_yamaha_vdp(personality)) { + this->fetch_line_buffer_->first_pixel_output_column += Storage::adjustment_[0]; + this->fetch_line_buffer_->next_border_column += Storage::adjustment_[0]; + } + + this->fetch_line_buffer_->vertical_state = + this->screen_mode_ == ScreenMode::Blank ? + VerticalState::Blank : + this->vertical_state(); + const bool is_refresh = this->fetch_line_buffer_->vertical_state == VerticalState::Blank; + + Storage::begin_line(this->screen_mode_, is_refresh); + + if(is_refresh) { + // The Yamaha handles refresh lines via its own microprogram; other VDPs + // can fall back on the regular refresh mechanic. + if constexpr (is_yamaha_vdp(personality)) { + this->fetch_line_buffer_->fetch_mode = FetchMode::Yamaha; + } else { + this->fetch_line_buffer_->fetch_mode = FetchMode::Refresh; + } + } + } + } + + +#ifndef NDEBUG + assert(backup.row == this->output_pointer_.row && backup.column == this->output_pointer_.column); + backup = this->fetch_pointer_; +#endif + + + if(output_cycles_pool) { + // Determine how much time has passed in the remainder of this line, and proceed. + const int target_output_cycles = std::min( + LineLayout::CyclesPerLine - this->output_pointer_.column, + output_cycles_pool + ); + int output_cycles_performed = 0; + uint32_t next_cram_value = 0; + + while(output_cycles_performed < target_output_cycles) { + int output_cycles = target_output_cycles - output_cycles_performed; + if(!output_cycles) continue; + + // Grab the next CRAM dot value and schedule a break in output if applicable. + const uint32_t cram_value = next_cram_value; + if constexpr (is_sega_vdp(personality)) { + next_cram_value = 0; + + if(!this->upcoming_cram_dots_.empty() && this->upcoming_cram_dots_.front().location.row == this->output_pointer_.row) { + int time_until_dot = this->upcoming_cram_dots_.front().location.column - this->output_pointer_.column; + + if(time_until_dot < output_cycles) { + output_cycles = time_until_dot; + next_cram_value = this->upcoming_cram_dots_.front().value; + this->upcoming_cram_dots_.erase(this->upcoming_cram_dots_.begin()); + } + } + } + + output_cycles_performed += output_cycles; + + const int end_column = this->output_pointer_.column + output_cycles; + + + // -------------------- + // Output video stream. + // -------------------- + +#define crt_convert(action, time) this->crt_.action(from_internal(time)) +#define output_sync(x) crt_convert(output_sync, x) +#define output_blank(x) crt_convert(output_blank, x) +#define output_default_colour_burst(x) crt_convert(output_default_colour_burst, x) + +#define intersect(left, right, code) { \ + const int start = std::max(this->output_pointer_.column, left); \ + const int end = std::min(end_column, right); \ + if(end > start) {\ + code;\ + }\ + } + +#define border(left, right) intersect(left, right, this->output_border(end - start, cram_value)) + + const auto left_blank = [&]() { + // Blanking region: output the entire sequence when the cursor + // crosses the start-of-border point. + if( + this->output_pointer_.column < LineLayout::EndOfLeftErase && + end_column >= LineLayout::EndOfLeftErase + ) { + output_sync(LineLayout::EndOfSync); + output_blank(LineLayout::StartOfColourBurst - LineLayout::EndOfSync); + output_default_colour_burst(LineLayout::EndOfColourBurst - LineLayout::StartOfColourBurst); + output_blank(LineLayout::EndOfLeftErase - LineLayout::EndOfColourBurst); + } + }; + + const auto right_blank = [&]() { + if(end_column == LineLayout::CyclesPerLine) { + output_blank(LineLayout::CyclesPerLine - LineLayout::EndOfRightBorder); + } + }; + + if(this->draw_line_buffer_->vertical_state != VerticalState::Pixels) { + if( + this->output_pointer_.row >= this->mode_timing_.first_vsync_line && + this->output_pointer_.row < this->mode_timing_.first_vsync_line + 4 + ) { + // Vertical sync. + // TODO: the Yamaha and Mega Drive both support interlaced video. + if(end_column == LineLayout::CyclesPerLine) { + output_sync(LineLayout::CyclesPerLine); + } + } else { + left_blank(); + border(LineLayout::EndOfLeftErase, LineLayout::EndOfRightBorder); + right_blank(); + } + } else { + left_blank(); + + // Left border. + border(LineLayout::EndOfLeftErase, this->draw_line_buffer_->first_pixel_output_column); + +#define draw(function, clock) { \ + const int relative_start = from_internal(start - this->draw_line_buffer_->first_pixel_output_column); \ + const int relative_end = from_internal(end - this->draw_line_buffer_->first_pixel_output_column); \ + if(relative_start == relative_end) break; \ + this->function; } + + // Pixel region. + intersect( + this->draw_line_buffer_->first_pixel_output_column, + this->draw_line_buffer_->next_border_column, + if(!this->asked_for_write_area_) { + this->asked_for_write_area_ = true; + + this->pixel_origin_ = this->pixel_target_ = reinterpret_cast( + this->crt_.begin_data(size_t(this->draw_line_buffer_->pixel_count)) + ); + } + + if(this->pixel_target_) { + if constexpr (is_yamaha_vdp(personality)) { + draw(draw_yamaha(0, relative_start, relative_end), Clock::Internal); // TODO: what is the correct 'y'? + } else { + switch(this->draw_line_buffer_->fetch_mode) { + case FetchMode::SMS: draw(draw_sms(relative_start, relative_end, cram_value), Clock::TMSPixel); break; + case FetchMode::Character: draw(draw_tms_character(relative_start, relative_end), Clock::TMSPixel); break; + case FetchMode::Text: draw(template draw_tms_text(relative_start, relative_end), Clock::TMSPixel); break; + + default: break; /* Dealt with elsewhere. */ + } + } + } + + if(end == this->draw_line_buffer_->next_border_column) { + const int length = this->draw_line_buffer_->next_border_column - this->draw_line_buffer_->first_pixel_output_column; + this->crt_.output_data(from_internal(length), size_t(this->draw_line_buffer_->pixel_count)); + this->pixel_origin_ = this->pixel_target_ = nullptr; + this->asked_for_write_area_ = false; + } + ); + +#undef draw + + // Right border. + border(this->draw_line_buffer_->next_border_column, LineLayout::EndOfRightBorder); + + right_blank(); + } + +#undef border +#undef intersect + +#undef crt_convert +#undef output_sync +#undef output_blank +#undef output_default_colour_burst + + + + // ------------- + // Advance time. + // ------------- + this->output_pointer_.column = end_column; + if(end_column == LineLayout::CyclesPerLine) { + // Advance line buffer. + this->advance(this->draw_line_buffer_); + } + } + + output_cycles_pool -= target_output_cycles; + if(this->output_pointer_.column == LineLayout::CyclesPerLine) { + this->output_pointer_.column = 0; + this->output_pointer_.row = (this->output_pointer_.row + 1) % this->mode_timing_.total_lines; + } + } + + assert(backup.row == this->fetch_pointer_.row && backup.column == this->fetch_pointer_.column); + } +} + +template +void Base::output_border(int cycles, [[maybe_unused]] uint32_t cram_dot) { + cycles = from_internal(cycles); + + uint32_t border_colour; + if constexpr (is_sega_vdp(personality)) { + border_colour = Storage::colour_ram_[16 + background_colour_]; + + if(cram_dot) { + // Four CRT cycles is one pixel width, so this doesn't need clock conversion. + // TODO: on the Mega Drive it may be only 3 colour cycles, depending on mode. + crt_.output_level(4, border_colour | cram_dot); + cycles -= 4; + } + } else { + border_colour = palette()[background_colour_]; + } + + if(!cycles) { + return; + } + + // If the border colour is 0, that can be communicated + // more efficiently as an explicit blank. + if(border_colour) { + crt_.output_level(cycles, border_colour); + } else { + crt_.output_blank(cycles); + } +} + +// MARK: - External interface. + +template +int Base::masked_address(int address) const { + if constexpr (is_yamaha_vdp(personality)) { + return address & 3; + } else { + return address & 1; + } +} + +template +void Base::write_vram(uint8_t value) { + write_phase_ = false; + + // Enqueue the write to occur at the next available slot. + read_ahead_buffer_ = value; + queued_access_ = MemoryAccess::Write; + minimum_access_column_ = fetch_pointer_.column + LineLayout::VRAMAccessDelay; +} + +template +void Base::commit_register(int reg, uint8_t value) { + if constexpr (is_yamaha_vdp(personality)) { + reg &= 0x3f; + } else if constexpr (is_sega_vdp(personality)) { + if(reg & 0x40) { + Storage::cram_is_selected_ = true; + return; + } + reg &= 0xf; + } else { + reg &= 0x7; + } + + // + // Generic TMS functionality. + // + switch(reg) { + case 0: + mode2_enable_ = value & 0x02; + break; + + case 1: + blank_display_ = !(value & 0x40); + generate_interrupts_ = value & 0x20; + mode1_enable_ = value & 0x10; + mode3_enable_ = value & 0x08; + sprites_16x16_ = value & 0x02; + sprites_magnified_ = value & 0x01; + + sprite_height_ = 8; + if(sprites_16x16_) sprite_height_ <<= 1; + if(sprites_magnified_) sprite_height_ <<= 1; + break; + + case 2: install_field<10>(pattern_name_address_, value); break; + case 3: install_field<6>(colour_table_address_, value); break; + case 4: install_field<11>(pattern_generator_table_address_, value); break; + case 5: install_field<7>(sprite_attribute_table_address_, value); break; + case 6: install_field<11>(sprite_generator_table_address_, value); break; + + case 7: + text_colour_ = value >> 4; + background_colour_ = value & 0xf; + break; + + default: break; + } + + // + // Sega extensions. + // + if constexpr (is_sega_vdp(personality)) { + switch(reg) { + default: break; + + case 0: + Storage::vertical_scroll_lock_ = value & 0x80; + Storage::horizontal_scroll_lock_ = value & 0x40; + Storage::hide_left_column_ = value & 0x20; + enable_line_interrupts_ = value & 0x10; + Storage::shift_sprites_8px_left_ = value & 0x08; + Storage::mode4_enable_ = value & 0x04; + break; + + case 2: + Storage::pattern_name_address_ = pattern_name_address_ | ((personality == TMS::SMSVDP) ? 0x000 : 0x400); + break; + + case 5: + Storage::sprite_attribute_table_address_ = sprite_attribute_table_address_ | ((personality == TMS::SMSVDP) ? 0x00 : 0x80); + break; + + case 6: + Storage::sprite_generator_table_address_ = sprite_generator_table_address_ | ((personality == TMS::SMSVDP) ? 0x0000 : 0x1800); + break; + + case 8: + Storage::horizontal_scroll_ = value; + break; + + case 9: + Storage::vertical_scroll_ = value; + break; + + case 10: + line_interrupt_target_ = value; + break; + } + } + + // + // Yamaha extensions. + // + if constexpr (is_yamaha_vdp(personality)) { + switch(reg) { + default: break; + + case 0: + Storage::mode_ = uint8_t( + (Storage::mode_ & 3) | + ((value & 0xe) << 1) + ); + enable_line_interrupts_ = value & 0x10; + + // b1–b3: M3–M5 + // b4: enable horizontal retrace interrupt + // b5: enable light pen interrupts + // b6: set colour bus to input or output mode + break; + + case 1: + Storage::mode_ = uint8_t( + (Storage::mode_ & 0x1c) | + ((value & 0x10) >> 4) | + ((value & 0x08) >> 2) + ); + break; + + case 7: + Storage::background_palette_[0] = Storage::palette_[background_colour_]; + break; + + case 8: + Storage::solid_background_ = value & 0x20; + Storage::sprites_enabled_ = !(value & 0x02); + if(value & 0x01) { + logger.error().append("TODO: Yamaha greyscale"); + } + // b7: "1 = input on colour bus, enable mouse; 1 = output on colour bus, disable mouse" [documentation clearly in error] + // b6: 1 = enable light pen + // b5: sets the colour of code 0 to the colour of the palette (???) + // b4: 1 = colour bus in input mode; 0 = colour bus in output mode + // b3: 1 = VRAM is 64kx1 or 64kx4; 0 = 16kx1 or 16kx4; affects refresh. + // b1: 1 = disable sprites (and release sprite access slots) + // b0: 1 = output in grayscale + break; + + case 9: + mode_timing_.pixel_lines = (value & 0x80) ? 212 : 192; + mode_timing_.end_of_frame_interrupt_position.row = mode_timing_.pixel_lines+1; + // TODO: on the Yamaha, at least, tie this interrupt overtly to vertical state. + + if(value & 0x08) { + logger.error().append("TODO: Yamaha interlace mode"); + } + + // b7: 1 = 212 lines of pixels; 0 = 192 + // b5 & b4: select simultaneous mode (seems to relate to line length and in-phase colour?) + // b3: 1 = interlace on + // b2: 1 = display two graphic screens interchangeably by even/odd field + // b1: 1 = PAL mode; 0 = NTSC mode + // b0: 1 = [dot clock] DLCLK is input; 0 = DLCLK is output + break; + + // b0–b2: A14–A16 of the colour table. + case 10: install_field<14>(colour_table_address_, value); break; + + // b0–b1: A15–A16 of the sprite table. + case 11: install_field<15>(sprite_attribute_table_address_, value); break; + + case 12: + Storage::blink_text_colour_ = value >> 4; + Storage::blink_background_colour_ = value & 0xf; + // as per register 7, but in blink mode. + break; + + case 13: + Storage::blink_periods_ = value; + if(!value) { + Storage::in_blink_ = 0; + } + + // b0–b3: display time for odd page; + // b4–b7: display time for even page. + break; + + case 14: install_field<14>(ram_pointer_, value); break; + + case 15: + Storage::selected_status_ = value & 0xf; + break; + + case 16: + Storage::palette_entry_ = value; + // b0–b3: palette entry for writing on port 2; autoincrements upon every write. + break; + + case 17: + Storage::increment_indirect_register_ = !(value & 0x80); + Storage::indirect_register_ = value & 0x3f; + break; + + case 18: + Storage::adjustment_[0] = (8 - ((value & 15) ^ 8)) * 4; + Storage::adjustment_[1] = 8 - ((value >> 4) ^ 8); + // b0-b3: horizontal adjustment + // b4-b7: vertical adjustment + break; + + case 19: + line_interrupt_target_ = value; + // b0–b7: line to match for interrupts (if eabled) + break; + + case 20: + case 21: + case 22: +// logger.error().append("TODO: Yamaha colour burst selection; %02x", value); + // Documentation is "fill with 0s for no colour burst; magic pattern for colour burst" + break; + + case 23: + Storage::vertical_offset_ = value; + break; + + case 32: Storage::command_context_.source.template set<0, false>(value); break; + case 33: Storage::command_context_.source.template set<0, true>(value); break; + case 34: Storage::command_context_.source.template set<1, false>(value); break; + case 35: Storage::command_context_.source.template set<1, true>(value); break; + + case 36: Storage::command_context_.destination.template set<0, false>(value); break; + case 37: Storage::command_context_.destination.template set<0, true>(value); break; + case 38: Storage::command_context_.destination.template set<1, false>(value); break; + case 39: Storage::command_context_.destination.template set<1, true>(value); break; + + case 40: Storage::command_context_.size.template set<0, false>(value); break; + case 41: Storage::command_context_.size.template set<0, true>(value); break; + case 42: Storage::command_context_.size.template set<1, false>(value); break; + case 43: Storage::command_context_.size.template set<1, true>(value); break; + + case 44: + Storage::command_context_.colour.set(value); + + // Check whether a command was blocked on this. + if( + Storage::command_ && + Storage::command_->access == Command::AccessType::WaitForColourReceipt + ) { + Storage::command_->advance(); + Storage::update_command_step(fetch_pointer_.column); + } + break; + + case 45: + Storage::command_context_.arguments = value; + // b6: MXC, i.e. destination for INed/OUTed video data; 0 = video RAM; 1 = expansion RAM. + // b5: MXD, destination for command engine. + // b4: MXS, source for command engine. + // b3: DIY + // b2: DIX + // b1: EQ + // b0: MAJ + break; + + case 46: + // b0–b3: LO0–LO3 (i.e. operation to apply if this is a logical command) + // b4–b7: CM0-CM3 (i.e. command to perform) + + // If a command is already ongoing and this is not a stop, ignore it. + if(Storage::command_ && (value >> 4) != 0b0000) { + break; + } + +#define Begin(x) Storage::command_ = std::make_unique(Storage::command_context_, Storage::mode_description_); + using MoveType = Commands::MoveType; + switch(value >> 4) { + // All codes not listed below are invalid; treat them as STOP. + default: + case 0b0000: Storage::command_ = nullptr; break; // STOP. + + case 0b0100: Begin(Point); break; // POINT [read a pixel colour]. + case 0b0101: Begin(Point); break; // PSET [plot a pixel]. + case 0b0110: break; // TODO: srch. [search horizontally for a colour] + case 0b0111: Begin(Line); break; // LINE [draw a Bresenham line]. + + case 0b1000: Begin(Fill); break; // LMMV [logical move, VDP to VRAM, i.e. solid-colour fill]. + case 0b1001: Begin(Move); break; // LMMM [logical move, VRAM to VRAM]. + case 0b1010: break; // TODO: lmcm. [logical move, VRAM to CPU] + case 0b1011: Begin(MoveFromCPU); break; // LMMC [logical move, CPU to VRAM]. + + case 0b1100: Begin(Fill); break; // HMMV [high-speed move, VDP to VRAM, i.e. single-byte fill]. + case 0b1101: Begin(Move); break; // HMMM [high-speed move, VRAM to VRAM]. + case 0b1110: Begin(Move); break; // YMMM [high-speed move, y only, VRAM to VRAM]. + case 0b1111: Begin(MoveFromCPU); break; // HMMC [high-speed move, CPU to VRAM]. + } +#undef Begin + + Storage::command_context_.pixel_operation = CommandContext::LogicalOperation(value & 7); + Storage::command_context_.test_source = value & 8; + + // Kill the command immediately if it's done in zero operations + // (e.g. a line of length 0). + if(!Storage::command_ && (value >> 4)) { + logger.error().append("TODO: Yamaha command %02x", value); + } + + // Seed timing information if a command was found. + Storage::update_command_step(fetch_pointer_.column); + break; + } + } +} + +template +void Base::write_register(uint8_t value) { + // Writes to address 1 are performed in pairs; if this is the + // low byte of a value, store it and wait for the high byte. + if(!write_phase_) { + low_write_ = value; + write_phase_ = true; + + // The initial write should half update the access pointer, other than + // on the Yamaha. + if constexpr (!is_yamaha_vdp(personality)) { + install_field<0>(ram_pointer_, value); + } + return; + } + + // The RAM pointer is always set on a second write, regardless of + // whether the caller is intending to enqueue a VDP operation. + // If this isn't a Yamaha VDP then the RAM address is updated + // regardless of whether this turns out to be a register access. + // + // The top two bits are used to determine the type of write; only + // the lower six are actual address. + if constexpr (!is_yamaha_vdp(personality)) { + install_field<8, 6>(ram_pointer_, value); + } + + write_phase_ = false; + if(value & 0x80) { + commit_register(value, low_write_); + } else { + // This is an access via the RAM pointer; if this is a Yamaha VDP then update + // the low 14-bits of the RAM pointer now. + if constexpr (is_yamaha_vdp(personality)) { + install_field<0>(ram_pointer_, low_write_); + install_field<8, 6>(ram_pointer_, value); + } + + if(!(value & 0x40)) { + // A read request is enqueued upon setting the address; conversely a write + // won't be enqueued unless and until some actual data is supplied. + queued_access_ = MemoryAccess::Read; + minimum_access_column_ = fetch_pointer_.column + LineLayout::VRAMAccessDelay; + } + + if constexpr (is_sega_vdp(personality)) { + Storage::cram_is_selected_ = false; + } + } +} + +template +void Base::write_palette(uint8_t value) { + if constexpr (is_yamaha_vdp(personality)) { + if(!Storage::palette_write_phase_) { + Storage::new_colour_ = value; + Storage::palette_write_phase_ = true; + return; + } + + Storage::palette_write_phase_ = false; + + const uint8_t r = ((Storage::new_colour_ >> 4) & 7) * 255 / 7; + const uint8_t g = (value & 7) * 255 / 7; + const uint8_t b = (Storage::new_colour_ & 7) * 255 / 7; + + Storage::palette_[Storage::palette_entry_ & 0xf] = palette_pack(r, g, b); + Storage::background_palette_[Storage::palette_entry_ & 0xf] = palette_pack(r, g, b); + Storage::background_palette_[0] = Storage::palette_[background_colour_]; + + ++Storage::palette_entry_; + } +} + +template +void Base::write_register_indirect([[maybe_unused]] uint8_t value) { + if constexpr (is_yamaha_vdp(personality)) { + // Register 17 cannot be written to indirectly. + if(Storage::indirect_register_ != 17) { + commit_register(Storage::indirect_register_, value); + } + Storage::indirect_register_ += Storage::increment_indirect_register_; + } +} + +template +void TMS9918::write(int address, uint8_t value) { + switch(this->masked_address(address)) { + default: break; + case 0: this->write_vram(value); break; + case 1: this->write_register(value); break; + case 2: this->write_palette(value); break; + case 3: this->write_register_indirect(value); break; + } +} + +template +uint8_t Base::read_vram() { + // Take whatever is currently in the read-ahead buffer and + // enqueue a further read to occur at the next available slot. + const uint8_t result = read_ahead_buffer_; + queued_access_ = MemoryAccess::Read; + return result; +} + +template +uint8_t Base::read_register() { + if constexpr (is_yamaha_vdp(personality)) { + switch(Storage::selected_status_) { + default: + case 0: break; + + case 1: { + // b7 = light pen; set when light is detected, reset on read; + // or: mouse button 2 currently down. + // b6 = light pen button or mouse button 1. + // b5–b1 = VDP identification (0 = 9938; 2 = 9958) + // b0 = set when the VDP reaches the line provided in the line interrupt register. + // Reset upon read. + const uint8_t result = + (personality == Personality::V9938 ? 0x0 : 0x4) | + ((line_interrupt_pending_ && enable_line_interrupts_) ? 0x01 : 0x00); + + line_interrupt_pending_ = false; + return result; + } break; + + case 2: { + // b7 = transfer ready flag (i.e. VDP ready for next transfer) + // b6 = 1 during vblank + // b5 = 1 during hblank + // b4 = set if colour detected during search command + // b1 = display field odd/even + // b0 = command ongoing + const uint8_t transfer_ready = + (queued_access_ == MemoryAccess::None ? 0x80 : 0x00) & + (( + !Storage::command_ || + !Storage::command_->is_cpu_transfer || + Storage::command_->access == Command::AccessType::WaitForColourReceipt + ) ? 0x80 : 0x00); + + return + transfer_ready | + (vertical_state() != VerticalState::Pixels ? 0x40 : 0x00) | + (is_horizontal_blank() ? 0x20 : 0x00) | + (Storage::command_ ? 0x01 : 0x00); + + } break; + + case 3: return uint8_t(Storage::collision_location_[0]); + case 4: return uint8_t((Storage::collision_location_[0] >> 8) | 0xfe); + case 5: return uint8_t(Storage::collision_location_[1]); + case 6: return uint8_t((Storage::collision_location_[1] >> 8) | 0xfc); + + case 7: return Storage::colour_status_; + + case 8: return uint8_t(Storage::colour_location_); + case 9: return uint8_t((Storage::colour_location_ >> 8) | 0xfe); + } + } + + // Gets the status register. + const uint8_t result = status_; + status_ &= ~(StatusInterrupt | StatusSpriteOverflow | StatusSpriteCollision); + if constexpr (is_sega_vdp(personality)) { + line_interrupt_pending_ = false; + } + return result; +} + +template +uint8_t TMS9918::read(int address) { + const int target = this->masked_address(address); + + if(target < 2) { + this->write_phase_ = false; + } + + switch(target) { + default: return 0xff; + case 0: return this->read_vram(); + case 1: return this->read_register(); + } +} + +// MARK: - Ephemeral state. + +template +int Base::fetch_line() const { + // This is the proper Master System value; TODO: what's correct for Yamaha, etc? + constexpr int row_change_position = 31; + + return + (this->fetch_pointer_.column < row_change_position) + ? (this->fetch_pointer_.row + this->mode_timing_.total_lines - 1) % this->mode_timing_.total_lines + : this->fetch_pointer_.row; +} + +template +VerticalState Base::vertical_state() const { + if(vertical_active_) { + return VerticalState::Pixels; + } else if(fetch_pointer_.row == mode_timing_.total_lines - 1) { + return VerticalState::Prefetch; + } else { + return VerticalState::Blank; + } +} + +template +bool Base::is_horizontal_blank() const { + return fetch_pointer_.column < LineLayout::EndOfLeftErase || fetch_pointer_.column >= LineLayout::EndOfRightBorder; +} + +template +uint8_t TMS9918::get_current_line() const { + int source_row = this->fetch_line(); + + if(this->tv_standard_ == TVStandard::NTSC) { + if(this->mode_timing_.pixel_lines == 240) { + // NTSC 256x240: 00-FF, 00-06 + } else if(this->mode_timing_.pixel_lines == 224) { + // NTSC 256x224: 00-EA, E5-FF + if(source_row >= 0xeb) source_row -= 6; + } else { + // NTSC 256x192: 00-DA, D5-FF + if(source_row >= 0xdb) source_row -= 6; + } + } else { + if(this->mode_timing_.pixel_lines == 240) { + // PAL 256x240: 00-FF, 00-0A, D2-FF + if(source_row >= 267) source_row -= 0x39; + } else if(this->mode_timing_.pixel_lines == 224) { + // PAL 256x224: 00-FF, 00-02, CA-FF + if(source_row >= 259) source_row -= 0x39; + } else { + // PAL 256x192: 00-F2, BA-FF + if(source_row >= 0xf3) source_row -= 0x39; + } + } + + return uint8_t(source_row); +} +template +HalfCycles TMS9918::next_sequence_point() const { + if(!this->generate_interrupts_ && !this->enable_line_interrupts_) return HalfCycles::max(); + if(get_interrupt_line()) return HalfCycles::max(); + + // Calculate the amount of time until the next end-of-frame interrupt. + const int frame_length = LineLayout::CyclesPerLine * this->mode_timing_.total_lines; + int time_until_frame_interrupt = + ( + ((this->mode_timing_.end_of_frame_interrupt_position.row * LineLayout::CyclesPerLine) + this->mode_timing_.end_of_frame_interrupt_position.column + frame_length) - + ((this->fetch_pointer_.row * LineLayout::CyclesPerLine) + this->fetch_pointer_.column) + ) % frame_length; + if(!time_until_frame_interrupt) time_until_frame_interrupt = frame_length; + + if(!this->enable_line_interrupts_) { + return this->clock_converter_.half_cycles_before_internal_cycles(time_until_frame_interrupt); + } + + // Calculate when the next line interrupt will occur. + int next_line_interrupt_row = -1; + + int cycles_to_next_interrupt_threshold = this->mode_timing_.line_interrupt_position - this->fetch_pointer_.column; + int line_of_next_interrupt_threshold = this->fetch_pointer_.row; + if(cycles_to_next_interrupt_threshold <= 0) { + cycles_to_next_interrupt_threshold += LineLayout::CyclesPerLine; + ++line_of_next_interrupt_threshold; + } + + if constexpr (is_sega_vdp(personality)) { + // If there is still time for a line interrupt this frame, that'll be it; + // otherwise it'll be on the next frame, supposing there's ever time for + // it at all. + if(line_of_next_interrupt_threshold + this->line_interrupt_counter_ <= this->mode_timing_.pixel_lines) { + next_line_interrupt_row = line_of_next_interrupt_threshold + this->line_interrupt_counter_; + } else { + if(this->line_interrupt_target_ <= this->mode_timing_.pixel_lines) + next_line_interrupt_row = this->mode_timing_.total_lines + this->line_interrupt_target_; + } + } + + if constexpr (is_yamaha_vdp(personality)) { + next_line_interrupt_row = (this->line_interrupt_target_ - Storage::vertical_offset_) & 0xff; + } + + // If there's actually no interrupt upcoming, despite being enabled, either return + // the frame end interrupt or no interrupt pending as appropriate. + if(next_line_interrupt_row == -1) { + return this->generate_interrupts_ ? + this->clock_converter_.half_cycles_before_internal_cycles(time_until_frame_interrupt) : + HalfCycles::max(); + } + + // Figure out the number of internal cycles until the next line interrupt, which is the amount + // of time to the next tick over and then next_line_interrupt_row - row_ lines further. + const int lines_until_interrupt = (next_line_interrupt_row - line_of_next_interrupt_threshold + this->mode_timing_.total_lines) % this->mode_timing_.total_lines; + const int local_cycles_until_line_interrupt = cycles_to_next_interrupt_threshold + lines_until_interrupt * LineLayout::CyclesPerLine; + if(!this->generate_interrupts_) return this->clock_converter_.half_cycles_before_internal_cycles(local_cycles_until_line_interrupt); + + // Return whichever interrupt is closer. + return this->clock_converter_.half_cycles_before_internal_cycles(std::min(local_cycles_until_line_interrupt, time_until_frame_interrupt)); +} + +template +HalfCycles TMS9918::get_time_until_line(int line) { + if(line < 0) line += this->mode_timing_.total_lines; + + int cycles_to_next_interrupt_threshold = this->mode_timing_.line_interrupt_position - this->fetch_pointer_.column; + int line_of_next_interrupt_threshold = this->fetch_pointer_.row; + if(cycles_to_next_interrupt_threshold <= 0) { + cycles_to_next_interrupt_threshold += LineLayout::CyclesPerLine; + ++line_of_next_interrupt_threshold; + } + + if(line_of_next_interrupt_threshold > line) { + line += this->mode_timing_.total_lines; + } + + return this->clock_converter_.half_cycles_before_internal_cycles(cycles_to_next_interrupt_threshold + (line - line_of_next_interrupt_threshold)*LineLayout::CyclesPerLine); +} + +template +bool TMS9918::get_interrupt_line() const { + return + ((this->status_ & StatusInterrupt) && this->generate_interrupts_) || + (this->enable_line_interrupts_ && this->line_interrupt_pending_); +} + +// TODO: [potentially] remove Master System timing assumptions in latch and get_latched below, if any other VDP uses these calls. +template uint8_t TMS9918::get_latched_horizontal_counter() const { + // Translate from internal numbering to the public numbering, + // which counts the 256 pixels as items 0–255, starts + // counting at -48, and returns only the top 8 bits of the number. + int public_counter = this->latched_column_ - LineLayout::EndOfLeftBorder; + if(public_counter < -46) public_counter += LineLayout::CyclesPerLine; + return uint8_t(public_counter >> 1); +} + +template +void TMS9918::latch_horizontal_counter() { + this->latched_column_ = this->fetch_pointer_.column; +} + +template class TI::TMS::TMS9918; +template class TI::TMS::TMS9918; +//template class TI::TMS::TMS9918; +template class TI::TMS::TMS9918; +template class TI::TMS::TMS9918; +//template class TI::TMS::TMS9918; +//template class TI::TMS::TMS9918; diff --git a/Components/9918/Implementation/9918Base.hpp b/Components/9918/Implementation/9918Base.hpp index c0bb8c86d..ec63e267f 100644 --- a/Components/9918/Implementation/9918Base.hpp +++ b/Components/9918/Implementation/9918Base.hpp @@ -6,355 +6,548 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef TMS9918Base_hpp -#define TMS9918Base_hpp +#pragma once + +#include "ClockConverter.hpp" -#include "../../../Outputs/CRT/CRT.hpp" #include "../../../ClockReceiver/ClockReceiver.hpp" +#include "../../../Numeric/BitReverse.hpp" +#include "../../../Outputs/CRT/CRT.hpp" +#include "AccessEnums.hpp" +#include "LineBuffer.hpp" +#include "PersonalityTraits.hpp" +#include "Storage.hpp" +#include "YamahaCommands.hpp" + +#include #include #include +#include #include #include -namespace TI { -namespace TMS { +namespace TI::TMS { -enum Personality { - TMS9918A, // includes the 9928 and 9929; set TV standard and output device as desired. - V9938, - V9958, - SMSVDP, - SMS2VDP, - GGVDP, -}; +constexpr uint8_t StatusInterrupt = 0x80; +constexpr uint8_t StatusSpriteOverflow = 0x40; -enum class TVStandard { - /*! i.e. 50Hz output at around 312.5 lines/field */ - PAL, - /*! i.e. 60Hz output at around 262.5 lines/field */ - NTSC -}; +constexpr int StatusSpriteCollisionShift = 5; +constexpr uint8_t StatusSpriteCollision = 0x20; -#define is_sega_vdp(x) ((x) >= SMSVDP) +template struct Base: public Storage { + Base(); -class Base { - public: - static uint32_t palette_pack(uint8_t r, uint8_t g, uint8_t b) { - uint32_t result = 0; - uint8_t *const result_ptr = reinterpret_cast(&result); - result_ptr[0] = r; - result_ptr[1] = g; - result_ptr[2] = b; - result_ptr[3] = 0; - return result; + static constexpr int output_lag = 11; // i.e. pixel output will occur 11 cycles + // after corresponding data read. + + static constexpr uint32_t palette_pack(uint8_t r, uint8_t g, uint8_t b) { + #if TARGET_RT_BIG_ENDIAN + return uint32_t((r << 24) | (g << 16) | (b << 8)); + #else + return uint32_t((b << 16) | (g << 8) | r); + #endif + } + + // The default TMS palette. + static constexpr std::array default_palette { + palette_pack(0, 0, 0), + palette_pack(0, 0, 0), + palette_pack(33, 200, 66), + palette_pack(94, 220, 120), + + palette_pack(84, 85, 237), + palette_pack(125, 118, 252), + palette_pack(212, 82, 77), + palette_pack(66, 235, 245), + + palette_pack(252, 85, 84), + palette_pack(255, 121, 120), + palette_pack(212, 193, 84), + palette_pack(230, 206, 128), + + palette_pack(33, 176, 59), + palette_pack(201, 91, 186), + palette_pack(204, 204, 204), + palette_pack(255, 255, 255) + }; + const std::array &palette() { + if constexpr (is_yamaha_vdp(personality)) { + return Storage::solid_background_ ? Storage::palette_ : Storage::background_palette_; } + return default_palette; + } - protected: - static constexpr int output_lag = 11; // i.e. pixel output will occur 11 cycles after corresponding data read. + Outputs::CRT::CRT crt_; + TVStandard tv_standard_ = TVStandard::NTSC; + using AddressT = typename Storage::AddressT; - // The default TMS palette. - const uint32_t palette[16] = { - palette_pack(0, 0, 0), - palette_pack(0, 0, 0), - palette_pack(33, 200, 66), - palette_pack(94, 220, 120), + /// Mutates @c target such that @c source replaces the @c length bits that currently start + /// at bit @c shift . Subsequently ensures @c target is constrained by the + /// applicable @c memory_mask. + template void install_field(AddressT &target, uint8_t source) { + static_assert(length > 0 && length <= 8); + constexpr auto source_mask = (1 << length) - 1; + constexpr auto mask = AddressT(~(source_mask << shift)); + target = ( + (target & mask) | + AddressT((source & source_mask) << shift) + ) & memory_mask(personality); + } - palette_pack(84, 85, 237), - palette_pack(125, 118, 252), - palette_pack(212, 82, 77), - palette_pack(66, 235, 245), + // Personality-specific metrics and converters. + ClockConverter clock_converter_; - palette_pack(252, 85, 84), - palette_pack(255, 121, 120), - palette_pack(212, 193, 84), - palette_pack(230, 206, 128), + // This VDP's DRAM. + std::array ram_; - palette_pack(33, 176, 59), - palette_pack(201, 91, 186), - palette_pack(204, 204, 204), - palette_pack(255, 255, 255) - }; + // State of the DRAM/CRAM-access mechanism. + AddressT ram_pointer_ = 0; + uint8_t read_ahead_buffer_ = 0; + MemoryAccess queued_access_ = MemoryAccess::None; + int minimum_access_column_ = 0; - Base(Personality p); + // The main status register. + uint8_t status_ = 0; - const Personality personality_; - Outputs::CRT::CRT crt_; - TVStandard tv_standard_ = TVStandard::NTSC; + // Current state of programmer input. + bool write_phase_ = false; // Determines whether the VDP is expecting the low or high byte of a write. + uint8_t low_write_ = 0; // Buffers the low byte of a write. - // Holds the contents of this VDP's connected DRAM. - std::vector ram_; + // Various programmable flags. + bool mode1_enable_ = false; + bool mode2_enable_ = false; + bool mode3_enable_ = false; + bool blank_display_ = false; + bool sprites_16x16_ = false; + bool sprites_magnified_ = false; + bool generate_interrupts_ = false; + uint8_t sprite_height_ = 8; - // Holds the state of the DRAM/CRAM-access mechanism. - uint16_t ram_pointer_ = 0; - uint8_t read_ahead_buffer_ = 0; - enum class MemoryAccess { - Read, Write, None - } queued_access_ = MemoryAccess::None; - int cycles_until_access_ = 0; - int minimum_access_column_ = 0; - int vram_access_delay() { - // This seems to be correct for all currently-modelled VDPs; - // it's the delay between an external device scheduling a - // read or write and the very first time that can occur - // (though, in practice, it won't happen until the next - // external slot after this number of cycles after the - // device has requested the read or write). - return 6; - } + // Programmer-specified addresses. + // + // The TMS and descendants combine various parts of the address with AND operations, + // e.g. the fourth byte in the pattern name table will be at `pattern_name_address_ & 4`; + // ordinarily the difference between that and plain substitution is invisible because + // the programmer mostly can't set low-enough-order bits. That's not universally true + // though, so this implementation uses AND throughout. + // + // ... therefore, all programmer-specified addresses are seeded as all '1's. As and when + // actual addresses are specified, the relevant bits will be substituted in. + // + // Cf. install_field. + AddressT pattern_name_address_ = memory_mask(personality); // Address of the tile map. + AddressT colour_table_address_ = memory_mask(personality); // Address of the colour map (if applicable). + AddressT pattern_generator_table_address_ = memory_mask(personality); // Address of the tile contents. + AddressT sprite_attribute_table_address_ = memory_mask(personality); // Address of the sprite list. + AddressT sprite_generator_table_address_ = memory_mask(personality); // Address of the sprite contents. - // Holds the main status register. - uint8_t status_ = 0; + // Default colours. + uint8_t text_colour_ = 0; + uint8_t background_colour_ = 0; - // Current state of programmer input. - bool write_phase_ = false; // Determines whether the VDP is expecting the low or high byte of a write. - uint8_t low_write_ = 0; // Buffers the low byte of a write. + // Internal mechanisms for position tracking. + int latched_column_ = 0; - // Various programmable flags. - bool mode1_enable_ = false; - bool mode2_enable_ = false; - bool mode3_enable_ = false; - bool blank_display_ = false; - bool sprites_16x16_ = false; - bool sprites_magnified_ = false; - bool generate_interrupts_ = false; - int sprite_height_ = 8; + // A struct to contain timing information that is a function of the current mode. + struct { + /* + Vertical layout: - size_t pattern_name_address_ = 0; // i.e. address of the tile map. - size_t colour_table_address_ = 0; // address of the colour map (if applicable). - size_t pattern_generator_table_address_ = 0; // address of the tile contents. - size_t sprite_attribute_table_address_ = 0; // address of the sprite list. - size_t sprite_generator_table_address_ = 0; // address of the sprite contents. + Lines 0 to [pixel_lines]: standard data fetch and drawing will occur. + ... to [first_vsync_line]: refresh fetches will occur and border will be output. + .. to [2.5 or 3 lines later]: vertical sync is output. + ... to [total lines - 1]: refresh fetches will occur and border will be output. + ... for one line: standard data fetch will occur, without drawing. + */ + int total_lines = 262; + int pixel_lines = 192; + int first_vsync_line = 227; - uint8_t text_colour_ = 0; - uint8_t background_colour_ = 0; + // Maximum number of sprite slots to populate; + // if sprites beyond this number should be visible + // then the appropriate status information will be set. + int maximum_visible_sprites = 4; - // This implementation of this chip officially accepts a 3.58Mhz clock, but runs - // internally at 5.37Mhz. The following two help to maintain a lossless conversion - // from the one to the other. - int cycles_error_ = 0; - HalfCycles half_cycles_before_internal_cycles(int internal_cycles); - - // Internal mechanisms for position tracking. - int latched_column_ = 0; - - // A helper function to output the current border colour for - // the number of cycles supplied. - void output_border(int cycles, uint32_t cram_dot); - - // A struct to contain timing information for the current mode. + // Set the position, in cycles, of the two interrupts, + // within a line. + // + // TODO: redetermine where this number came from. struct { - /* - Vertical layout: + int column = 313; + int row = 192; + } end_of_frame_interrupt_position; + int line_interrupt_position = -1; - Lines 0 to [pixel_lines]: standard data fetch and drawing will occur. - ... to [first_vsync_line]: refresh fetches will occur and border will be output. - .. to [2.5 or 3 lines later]: vertical sync is output. - ... to [total lines - 1]: refresh fetches will occur and border will be output. - ... for one line: standard data fetch will occur, without drawing. - */ - int total_lines = 262; - int pixel_lines = 192; - int first_vsync_line = 227; - - // Maximum number of sprite slots to populate; - // if sprites beyond this number should be visible - // then the appropriate status information will be set. - int maximum_visible_sprites = 4; - - // Set the position, in cycles, of the two interrupts, - // within a line. - struct { - int column = 4; - int row = 193; - } end_of_frame_interrupt_position; - int line_interrupt_position = -1; - - // Enables or disabled the recognition of the sprite - // list terminator, and sets the terminator value. - bool allow_sprite_terminator = true; - uint8_t sprite_terminator = 0xd0; - } mode_timing_; - - uint8_t line_interrupt_target = 0xff; - uint8_t line_interrupt_counter = 0; - bool enable_line_interrupts_ = false; - bool line_interrupt_pending_ = false; - - // The screen mode is a necessary predecessor to picking the line mode, - // which is the thing latched per line. - enum class ScreenMode { - Blank, - Text, - MultiColour, - ColouredText, - Graphics, - SMSMode4 - } screen_mode_; - - enum class LineMode { - Text, - Character, - Refresh, - SMS - }; - - // Temporary buffers collect a representation of this line prior to pixel serialisation. - struct LineBuffer { - // The line mode describes the proper timing diagram for this line. - LineMode line_mode = LineMode::Text; - - // Holds the horizontal scroll position to apply to this line; - // of those VDPs currently implemented, affects the Master System only. - uint8_t latched_horizontal_scroll = 0; - - // The names array holds pattern names, as an offset into memory, and - // potentially flags also. - struct { - size_t offset = 0; - uint8_t flags = 0; - } names[40]; - - // The patterns array holds tile patterns, corresponding 1:1 with names. - // Four bytes per pattern is the maximum required by any - // currently-implemented VDP. - uint8_t patterns[40][4]; - - /* - Horizontal layout (on a 342-cycle clock): - - 15 cycles right border - 58 cycles blanking & sync - 13 cycles left border - - ... i.e. to cycle 86, then: - - border up to first_pixel_output_column; - pixels up to next_border_column; - border up to the end. - - e.g. standard 256-pixel modes will want to set - first_pixel_output_column = 86, next_border_column = 342. - */ - int first_pixel_output_column = 94; - int next_border_column = 334; - - // An active sprite is one that has been selected for composition onto - // this line. - struct ActiveSprite { - int index = 0; // The original in-table index of this sprite. - int row = 0; // The row of the sprite that should be drawn. - int x = 0; // The sprite's x position on screen. - - uint8_t image[4]; // Up to four bytes of image information. - int shift_position = 0; // An offset representing how much of the image information has already been drawn. - } active_sprites[8]; - - int active_sprite_slot = 0; // A pointer to the slot into which a new active sprite will be deposited, if required. - bool sprites_stopped = false; // A special TMS feature is that a sentinel value can be used to prevent any further sprites - // being evaluated for display. This flag determines whether the sentinel has yet been reached. - - void reset_sprite_collection(); - } line_buffers_[313]; - void posit_sprite(LineBuffer &buffer, int sprite_number, int sprite_y, int screen_row); - - // There is a delay between reading into the line buffer and outputting from there to the screen. That delay - // is observeable because reading time affects availability of memory accesses and therefore time in which - // to update sprites and tiles, but writing time affects when the palette is used and when the collision flag - // may end up being set. So the two processes are slightly decoupled. The end of reading one line may overlap - // with the beginning of writing the next, hence the two separate line buffers. - struct LineBufferPointer { - int row, column; - } read_pointer_, write_pointer_; - - // The SMS VDP has a programmer-set colour palette, with a dedicated patch of RAM. But the RAM is only exactly - // fast enough for the pixel clock. So when the programmer writes to it, that causes a one-pixel glitch; there - // isn't the bandwidth for the read both write to occur simultaneously. The following buffer therefore keeps - // track of pending collisions, for visual reproduction. - struct CRAMDot { - LineBufferPointer location; - uint32_t value; - }; - std::vector upcoming_cram_dots_; - - // Extra information that affects the Master System output mode. - struct { - // Programmer-set flags. - bool vertical_scroll_lock = false; - bool horizontal_scroll_lock = false; - bool hide_left_column = false; - bool shift_sprites_8px_left = false; - bool mode4_enable = false; - uint8_t horizontal_scroll = 0; - uint8_t vertical_scroll = 0; - - // The Master System's additional colour RAM. - uint32_t colour_ram[32]; - bool cram_is_selected = false; - - // Holds the vertical scroll position for this frame; this is latched - // once and cannot dynamically be changed until the next frame. - uint8_t latched_vertical_scroll = 0; - - size_t pattern_name_address; - size_t sprite_attribute_table_address; - size_t sprite_generator_table_address; - } master_system_; - - void set_current_screen_mode() { - if(blank_display_) { - screen_mode_ = ScreenMode::Blank; - return; + // Enables or disabled the recognition of the sprite + // list terminator, and sets the terminator value. + bool allow_sprite_terminator = true; + uint8_t sprite_terminator(ScreenMode mode) { + switch(mode) { + default: return 0xd0; + case ScreenMode::YamahaGraphics3: + case ScreenMode::YamahaGraphics4: + case ScreenMode::YamahaGraphics5: + case ScreenMode::YamahaGraphics6: + case ScreenMode::YamahaGraphics7: + return 0xd8; } + } + } mode_timing_; - if(is_sega_vdp(personality_) && master_system_.mode4_enable) { - screen_mode_ = ScreenMode::SMSMode4; - mode_timing_.maximum_visible_sprites = 8; - return; - } + uint8_t line_interrupt_target_ = 0xff; + uint8_t line_interrupt_counter_ = 0; + bool enable_line_interrupts_ = false; + bool line_interrupt_pending_ = false; + bool vertical_active_ = false; - mode_timing_.maximum_visible_sprites = 4; - if(!mode1_enable_ && !mode2_enable_ && !mode3_enable_) { - screen_mode_ = ScreenMode::ColouredText; - return; - } + ScreenMode screen_mode_, underlying_mode_; - if(mode1_enable_ && !mode2_enable_ && !mode3_enable_) { - screen_mode_ = ScreenMode::Text; - return; - } + using LineBufferArray = std::array; + LineBufferArray line_buffers_; + LineBufferArray::iterator fetch_line_buffer_; + LineBufferArray::iterator draw_line_buffer_; + void advance(LineBufferArray::iterator &iterator) { + ++iterator; + if(iterator == line_buffers_.end()) { + iterator = line_buffers_.begin(); + } + } - if(!mode1_enable_ && mode2_enable_ && !mode3_enable_) { - screen_mode_ = ScreenMode::Graphics; - return; - } + using SpriteBufferArray = std::array; + SpriteBufferArray sprite_buffers_; + SpriteBufferArray::iterator fetch_sprite_buffer_; + SpriteBuffer *fetched_sprites_ = nullptr; + void advance(SpriteBufferArray::iterator &iterator) { + ++iterator; + if(iterator == sprite_buffers_.end()) { + iterator = sprite_buffers_.begin(); + } + } + void regress(SpriteBufferArray::iterator &iterator) { + if(iterator == sprite_buffers_.begin()) { + iterator = sprite_buffers_.end(); + } + --iterator; + } - if(!mode1_enable_ && !mode2_enable_ && mode3_enable_) { - screen_mode_ = ScreenMode::MultiColour; - return; - } + AddressT tile_offset_ = 0; + uint8_t name_[4]{}; + void posit_sprite(int sprite_number, int sprite_y, uint8_t screen_row); - // TODO: undocumented TMS modes. - screen_mode_ = ScreenMode::Blank; + // There is a delay between reading into the line buffer and outputting from there to the screen. That delay + // is observeable because reading time affects availability of memory accesses and therefore time in which + // to update sprites and tiles, but writing time affects when the palette is used and when the collision flag + // may end up being set. So the two processes are slightly decoupled. The end of reading one line may overlap + // with the beginning of writing the next, hence the two separate line buffers. + LineBufferPointer output_pointer_, fetch_pointer_; + + int fetch_line() const; + bool is_horizontal_blank() const; + VerticalState vertical_state() const; + + int masked_address(int address) const; + void write_vram(uint8_t); + void write_register(uint8_t); + void write_palette(uint8_t); + void write_register_indirect(uint8_t); + uint8_t read_vram(); + uint8_t read_register(); + + void commit_register(int reg, uint8_t value); + + template ScreenMode current_screen_mode() const { + if(check_blank && blank_display_) { + return ScreenMode::Blank; } - void do_external_slot(int access_column) { - // Don't do anything if the required time for the access to become executable - // has yet to pass. - if(access_column < minimum_access_column_) { - return; + if constexpr (is_sega_vdp(personality)) { + if(Storage::mode4_enable_) { + return ScreenMode::SMSMode4; + } + } + + if constexpr (is_yamaha_vdp(personality)) { + switch(Storage::mode_) { + case 0b00001: return ScreenMode::Text; + case 0b01001: return ScreenMode::YamahaText80; + case 0b00010: return ScreenMode::MultiColour; + case 0b00000: return ScreenMode::YamahaGraphics1; + case 0b00100: return ScreenMode::YamahaGraphics2; + case 0b01000: return ScreenMode::YamahaGraphics3; + case 0b01100: return ScreenMode::YamahaGraphics4; + case 0b10000: return ScreenMode::YamahaGraphics5; + case 0b10100: return ScreenMode::YamahaGraphics6; + case 0b11100: return ScreenMode::YamahaGraphics7; + } + } + + if(!mode1_enable_ && !mode2_enable_ && !mode3_enable_) { + return ScreenMode::ColouredText; + } + + if(mode1_enable_ && !mode2_enable_ && !mode3_enable_) { + return ScreenMode::Text; + } + + if(!mode1_enable_ && mode2_enable_ && !mode3_enable_) { + return ScreenMode::Graphics; + } + + if(!mode1_enable_ && !mode2_enable_ && mode3_enable_) { + return ScreenMode::MultiColour; + } + + // TODO: undocumented TMS modes. + return ScreenMode::Blank; + } + + static AddressT rotate(AddressT address) { + return AddressT((address >> 1) | (address << 16)) & memory_mask(personality); + } + + AddressT command_address(Vector location, bool expansion) const { + if constexpr (is_yamaha_vdp(personality)) { + switch(this->underlying_mode_) { + default: + case ScreenMode::YamahaGraphics4: // 256 pixels @ 4bpp + return AddressT( + ((location.v[0] >> 1) & 127) + + (location.v[1] << 7) + ); + + case ScreenMode::YamahaGraphics5: // 512 pixels @ 2bpp + return AddressT( + ((location.v[0] >> 2) & 127) + + (location.v[1] << 7) + ); + + case ScreenMode::YamahaGraphics6: { // 512 pixels @ 4bpp + const auto linear_address = + AddressT( + ((location.v[0] >> 1) & 255) + + (location.v[1] << 8) + ); + return expansion ? linear_address : rotate(linear_address); + } + + case ScreenMode::YamahaGraphics7: { // 256 pixels @ 8bpp + const auto linear_address = + AddressT( + ((location.v[0] >> 0) & 255) + + (location.v[1] << 8) + ); + return expansion ? linear_address : rotate(linear_address); + } + } + } else { + return 0; + } + } + + uint8_t extract_colour(uint8_t byte, Vector location) const { + switch(this->screen_mode_) { + default: + case ScreenMode::YamahaGraphics4: // 256 pixels @ 4bpp + case ScreenMode::YamahaGraphics6: // 512 pixels @ 4bpp + return (byte >> (((location.v[0] & 1) ^ 1) << 2)) & 0xf; + + case ScreenMode::YamahaGraphics5: // 512 pixels @ 2bpp + return (byte >> (((location.v[0] & 3) ^ 3) << 1)) & 0x3; + + case ScreenMode::YamahaGraphics7: // 256 pixels @ 8bpp + return byte; + } + } + + std::pair command_colour_mask(Vector location) const { + if constexpr (is_yamaha_vdp(personality)) { + auto &context = Storage::command_context_; + auto colour = context.latched_colour.has_value() ? context.latched_colour : context.colour; + + switch(this->screen_mode_) { + default: + case ScreenMode::YamahaGraphics4: // 256 pixels @ 4bpp + case ScreenMode::YamahaGraphics6: // 512 pixels @ 4bpp + return + std::make_pair( + 0xf0 >> ((location.v[0] & 1) << 2), + colour.colour4bpp + ); + + case ScreenMode::YamahaGraphics5: // 512 pixels @ 2bpp + return + std::make_pair( + 0xc0 >> ((location.v[0] & 3) << 1), + colour.colour2bpp + ); + + case ScreenMode::YamahaGraphics7: // 256 pixels @ 8bpp + return + std::make_pair( + 0xff, + colour.colour + ); + } + } else { + return std::make_pair(0, 0); + } + } + + void do_external_slot(int access_column) { + // Don't do anything if the required time for the access to become executable + // has yet to pass. + if(queued_access_ == MemoryAccess::None || access_column < minimum_access_column_) { + if constexpr (is_yamaha_vdp(personality)) { + using CommandStep = typename Storage::CommandStep; + + if( + Storage::next_command_step_ == CommandStep::None || + access_column < Storage::minimum_command_column_ + ) { + return; + } + + auto &context = Storage::command_context_; + const uint8_t *const source = (context.arguments & 0x10) ? Storage::expansion_ram_.data() : ram_.data(); + const AddressT source_mask = (context.arguments & 0x10) ? 0xfff : 0x1ffff; + uint8_t *const destination = (context.arguments & 0x20) ? Storage::expansion_ram_.data() : ram_.data(); + const AddressT destination_mask = (context.arguments & 0x20) ? 0xfff : 0x1ffff; + switch(Storage::next_command_step_) { + // Duplicative, but keeps the compiler happy. + case CommandStep::None: + break; + + case CommandStep::CopySourcePixelToStatus: + Storage::colour_status_ = + extract_colour( + source[command_address(context.source, context.arguments & 0x10) & source_mask], + context.source + ); + + Storage::command_->advance(); + Storage::update_command_step(access_column); + break; + + case CommandStep::ReadSourcePixel: + context.latched_colour.set( + extract_colour( + source[command_address(context.source, context.arguments & 0x10)] & source_mask, + context.source) + ); + + Storage::minimum_command_column_ = access_column + 32; + Storage::next_command_step_ = CommandStep::ReadDestinationPixel; + break; + + case CommandStep::ReadDestinationPixel: + Storage::command_latch_ = + source[command_address(context.destination, context.arguments & 0x20) & source_mask]; + + Storage::minimum_command_column_ = access_column + 24; + Storage::next_command_step_ = CommandStep::WritePixel; + break; + + case CommandStep::WritePixel: { + const auto [mask, unmasked_colour] = command_colour_mask(context.destination); + const auto address = command_address(context.destination, context.arguments & 0x20) & destination_mask; + const uint8_t colour = unmasked_colour & mask; + context.latched_colour.reset(); + + using LogicalOperation = CommandContext::LogicalOperation; + if(!context.test_source || colour) { + switch(context.pixel_operation) { + default: + case LogicalOperation::Copy: + Storage::command_latch_ &= ~mask; + Storage::command_latch_ |= colour; + break; + case LogicalOperation::And: + Storage::command_latch_ &= ~mask | colour; + break; + case LogicalOperation::Or: + Storage::command_latch_ |= colour; + break; + case LogicalOperation::Xor: + Storage::command_latch_ ^= colour; + break; + case LogicalOperation::Not: + Storage::command_latch_ &= ~mask; + Storage::command_latch_ |= colour ^ mask; + break; + } + } + + destination[address] = Storage::command_latch_; + + Storage::command_->advance(); + Storage::update_command_step(access_column); + } break; + + case CommandStep::ReadSourceByte: { + Vector source_vector = context.source; + if(Storage::command_->y_only) { + source_vector.v[0] = context.destination.v[0]; + } + context.latched_colour.set(source[command_address(source_vector, context.arguments & 0x10) & source_mask]); + + Storage::minimum_command_column_ = access_column + 24; + Storage::next_command_step_ = CommandStep::WriteByte; + } break; + + case CommandStep::WriteByte: + destination[command_address(context.destination, context.arguments & 0x20) & destination_mask] + = context.latched_colour.has_value() ? context.latched_colour.colour : context.colour.colour; + context.latched_colour.reset(); + + Storage::command_->advance(); + Storage::update_command_step(access_column); + break; + } } - switch(queued_access_) { - default: return; + return; + } - case MemoryAccess::Write: - if(master_system_.cram_is_selected) { + // Copy and mutate the RAM pointer. + AddressT address = ram_pointer_; + ++ram_pointer_; + + // Determine the relevant RAM and its mask. + uint8_t *ram = ram_.data(); + AddressT mask = memory_mask(personality); + + if constexpr (is_yamaha_vdp(personality)) { + // The Yamaha increments only 14 bits of the address in TMS-compatible modes. + if(this->underlying_mode_ < ScreenMode::YamahaText80) { + ram_pointer_ = (ram_pointer_ & 0x3fff) | (address & AddressT(~0x3fff)); + } + + if(this->underlying_mode_ == ScreenMode::YamahaGraphics6 || this->underlying_mode_ == ScreenMode::YamahaGraphics7) { + // Rotate address one to the right as the hardware accesses + // the underlying banks of memory alternately but presents + // them as if linear. + address = rotate(address); + } + + // Also check whether expansion RAM is the true target here. + if(Storage::command_context_.arguments & 0x40) { + ram = Storage::expansion_ram_.data(); + mask = AddressT(Storage::expansion_ram_.size() - 1); + } + } + + switch(queued_access_) { + default: break; + + case MemoryAccess::Write: + if constexpr (is_sega_vdp(personality)) { + if(Storage::cram_is_selected_) { // Adjust the palette. In a Master System blue has a slightly different // scale; cf. https://www.retrorgb.com/sega-master-system-non-linear-blue-channel-findings.html constexpr uint8_t rg_scale[] = {0, 85, 170, 255}; constexpr uint8_t b_scale[] = {0, 104, 170, 255}; - master_system_.colour_ram[ram_pointer_ & 0x1f] = palette_pack( + Storage::colour_ram_[address & 0x1f] = palette_pack( rg_scale[(read_ahead_buffer_ >> 0) & 3], rg_scale[(read_ahead_buffer_ >> 2) & 3], b_scale[(read_ahead_buffer_ >> 4) & 3] @@ -363,11 +556,9 @@ class Base { // Schedule a CRAM dot; this is scheduled for wherever it should appear // on screen. So it's wherever the output stream would be now. Which // is output_lag cycles ago from the point of view of the input stream. - upcoming_cram_dots_.emplace_back(); - CRAMDot &dot = upcoming_cram_dots_.back(); - - dot.location.column = write_pointer_.column - output_lag; - dot.location.row = write_pointer_.row; + auto &dot = Storage::upcoming_cram_dots_.emplace_back(); + dot.location.column = fetch_pointer_.column - output_lag; + dot.location.row = fetch_pointer_.row; // Handle before this row conditionally; then handle after (or, more realistically, // exactly at the end of) naturally. @@ -378,486 +569,57 @@ class Base { dot.location.row += dot.location.column / 342; dot.location.column %= 342; - dot.value = master_system_.colour_ram[ram_pointer_ & 0x1f]; - } else { - ram_[ram_pointer_ & 16383] = read_ahead_buffer_; + dot.value = Storage::colour_ram_[address & 0x1f]; + break; } - break; - case MemoryAccess::Read: - read_ahead_buffer_ = ram_[ram_pointer_ & 16383]; - break; - } - ++ram_pointer_; - queued_access_ = MemoryAccess::None; + } + ram[address & mask] = read_ahead_buffer_; + break; + case MemoryAccess::Read: + read_ahead_buffer_ = ram[address & mask]; + break; } - -/* - Fetching routines follow below; they obey the following rules: - - 1) input is a start position and an end position; they should perform the proper - operations for the period: start <= time < end. - 2) times are measured relative to a 172-cycles-per-line clock (so: they directly - count access windows on the TMS and Master System). - 3) time 0 is the beginning of the access window immediately after the last pattern/data - block fetch that would contribute to this line, in a normal 32-column mode. So: - - * it's cycle 309 on Mattias' TMS diagram; - * it's cycle 1238 on his V9938 diagram; - * it's after the last background render block in Mask of Destiny's Master System timing diagram. - - That division point was selected, albeit arbitrarily, because it puts all the tile - fetches for a single line into the same [0, 171] period. - - 4) all of these functions are templated with a `use_end` parameter. That will be true if - end is < 172, false otherwise. So functions can use it to eliminate should-exit-not checks, - for the more usual path of execution. - - Provided for the benefit of the methods below: - - * the function external_slot(), which will perform any pending VRAM read/write. - * the macros slot(n) and external_slot(n) which can be used to schedule those things inside a - switch(start)-based implementation. - - All functions should just spool data to intermediary storage. This is because for most VDPs there is - a decoupling between fetch pattern and output pattern, and it's neater to keep the same division - for the exceptions. -*/ - -#define slot(n) \ - if(use_end && end == n) return; \ - [[fallthrough]]; \ - case n - -#define external_slot(n) \ - slot(n): do_external_slot((n)*2); - -#define external_slots_2(n) \ - external_slot(n); \ - external_slot(n+1); - -#define external_slots_4(n) \ - external_slots_2(n); \ - external_slots_2(n+2); - -#define external_slots_8(n) \ - external_slots_4(n); \ - external_slots_4(n+4); - -#define external_slots_16(n) \ - external_slots_8(n); \ - external_slots_8(n+8); - -#define external_slots_32(n) \ - external_slots_16(n); \ - external_slots_16(n+16); - - -/*********************************************** - TMS9918 Fetching Code -************************************************/ - - template void fetch_tms_refresh(int start, int end) { -#define refresh(location) \ - slot(location): \ - external_slot(location+1); - -#define refreshes_2(location) \ - refresh(location); \ - refresh(location+2); - -#define refreshes_4(location) \ - refreshes_2(location); \ - refreshes_2(location+4); - -#define refreshes_8(location) \ - refreshes_4(location); \ - refreshes_4(location+8); - - switch(start) { - default: assert(false); - - /* 44 external slots */ - external_slots_32(0) - external_slots_8(32) - external_slots_4(40) - - /* 64 refresh/external slot pairs (= 128 windows) */ - refreshes_8(44); - refreshes_8(60); - refreshes_8(76); - refreshes_8(92); - refreshes_8(108); - refreshes_8(124); - refreshes_8(140); - refreshes_8(156); - - return; - } - -#undef refreshes_8 -#undef refreshes_4 -#undef refreshes_2 -#undef refresh - } - - template void fetch_tms_text(int start, int end) { -#define fetch_tile_name(location, column) slot(location): line_buffer.names[column].offset = ram_[row_base + column]; -#define fetch_tile_pattern(location, column) slot(location): line_buffer.patterns[column][0] = ram_[row_offset + size_t(line_buffer.names[column].offset << 3)]; - -#define fetch_column(location, column) \ - fetch_tile_name(location, column); \ - external_slot(location+1); \ - fetch_tile_pattern(location+2, column); - -#define fetch_columns_2(location, column) \ - fetch_column(location, column); \ - fetch_column(location+3, column+1); - -#define fetch_columns_4(location, column) \ - fetch_columns_2(location, column); \ - fetch_columns_2(location+6, column+2); - -#define fetch_columns_8(location, column) \ - fetch_columns_4(location, column); \ - fetch_columns_4(location+12, column+4); - - LineBuffer &line_buffer = line_buffers_[write_pointer_.row]; - const size_t row_base = pattern_name_address_ & (0x3c00 | size_t(write_pointer_.row >> 3) * 40); - const size_t row_offset = pattern_generator_table_address_ & (0x3800 | (write_pointer_.row & 7)); - - switch(start) { - default: assert(false); - - /* 47 external slots (= 47 windows) */ - external_slots_32(0) - external_slots_8(32) - external_slots_4(40) - external_slots_2(44) - external_slot(46) - - /* 40 column fetches (= 120 windows) */ - fetch_columns_8(47, 0); - fetch_columns_8(71, 8); - fetch_columns_8(95, 16); - fetch_columns_8(119, 24); - fetch_columns_8(143, 32); - - /* 5 more external slots */ - external_slots_4(167); - external_slot(171); - - return; - } - -#undef fetch_columns_8 -#undef fetch_columns_4 -#undef fetch_columns_2 -#undef fetch_column -#undef fetch_tile_pattern -#undef fetch_tile_name - } - - template void fetch_tms_character(int start, int end) { -#define sprite_fetch_coordinates(location, sprite) \ - slot(location): \ - slot(location+1): \ - line_buffer.active_sprites[sprite].x = \ - ram_[\ - sprite_attribute_table_address_ & size_t(0x3f81 | (line_buffer.active_sprites[sprite].index << 2))\ - ]; - - // This implementation doesn't refetch Y; it's unclear to me - // whether it's refetched. - -#define sprite_fetch_graphics(location, sprite) \ - slot(location): \ - slot(location+1): \ - slot(location+2): \ - slot(location+3): {\ - const uint8_t name = ram_[\ - sprite_attribute_table_address_ & size_t(0x3f82 | (line_buffer.active_sprites[sprite].index << 2))\ - ] & (sprites_16x16_ ? ~3 : ~0);\ - line_buffer.active_sprites[sprite].image[2] = ram_[\ - sprite_attribute_table_address_ & size_t(0x3f83 | (line_buffer.active_sprites[sprite].index << 2))\ - ];\ - line_buffer.active_sprites[sprite].x -= (line_buffer.active_sprites[sprite].image[2] & 0x80) >> 2;\ - const size_t graphic_location = sprite_generator_table_address_ & size_t(0x3800 | (name << 3) | line_buffer.active_sprites[sprite].row); \ - line_buffer.active_sprites[sprite].image[0] = ram_[graphic_location];\ - line_buffer.active_sprites[sprite].image[1] = ram_[graphic_location+16];\ + queued_access_ = MemoryAccess::None; } -#define sprite_fetch_block(location, sprite) \ - sprite_fetch_coordinates(location, sprite) \ - sprite_fetch_graphics(location+2, sprite) + /// Helper for TMS dispatches; contains a switch statement with cases 0 to 170, each of the form: + /// + /// if constexpr (use_end && end == n) return; [[fallthrough]]; case n: fetcher.fetch(); + /// + /// i.e. it provides standard glue to enter a fetch sequence at any point, while the fetches themselves are templated on the cycle + /// at which they appear for neater expression. + template void dispatch(Fetcher &fetcher, int start, int end); -#define sprite_y_read(location, sprite) \ - slot(location): posit_sprite(sprite_selection_buffer, sprite, ram_[sprite_attribute_table_address_ & (((sprite) << 2) | 0x3f80)], write_pointer_.row); + // Various fetchers. + template void fetch_tms_refresh(uint8_t y, int start, int end); + template void fetch_tms_text(uint8_t y, int start, int end); + template void fetch_tms_character(uint8_t y, int start, int end); -#define fetch_tile_name(column) line_buffer.names[column].offset = ram_[(row_base + column) & 0x3fff]; + template void fetch_yamaha(uint8_t y, int start, int end); + template void fetch_yamaha(uint8_t y, int end); -#define fetch_tile(column) {\ - line_buffer.patterns[column][1] = ram_[(colour_base + size_t((line_buffer.names[column].offset << 3) >> colour_name_shift)) & 0x3fff]; \ - line_buffer.patterns[column][0] = ram_[(pattern_base + size_t(line_buffer.names[column].offset << 3)) & 0x3fff]; \ - } + template void fetch_sms(uint8_t y, int start, int end); -#define background_fetch_block(location, column, sprite) \ - slot(location): fetch_tile_name(column) \ - external_slot(location+1); \ - slot(location+2): \ - slot(location+3): fetch_tile(column) \ - slot(location+4): fetch_tile_name(column+1) \ - sprite_y_read(location+5, sprite); \ - slot(location+6): \ - slot(location+7): fetch_tile(column+1) \ - slot(location+8): fetch_tile_name(column+2) \ - sprite_y_read(location+9, sprite+1); \ - slot(location+10): \ - slot(location+11): fetch_tile(column+2) \ - slot(location+12): fetch_tile_name(column+3) \ - sprite_y_read(location+13, sprite+2); \ - slot(location+14): \ - slot(location+15): fetch_tile(column+3) + // A helper function to output the current border colour for + // the number of cycles supplied. + void output_border(int cycles, uint32_t cram_dot); - LineBuffer &line_buffer = line_buffers_[write_pointer_.row]; - LineBuffer &sprite_selection_buffer = line_buffers_[(write_pointer_.row + 1) % mode_timing_.total_lines]; - const size_t row_base = pattern_name_address_ & (size_t((write_pointer_.row << 2)&~31) | 0x3c00); + // Output serialisation state. + uint32_t *pixel_target_ = nullptr, *pixel_origin_ = nullptr; + bool asked_for_write_area_ = false; - size_t pattern_base = pattern_generator_table_address_; - size_t colour_base = colour_table_address_; - int colour_name_shift = 6; + // Output serialisers. + template void draw_tms_character(int start, int end); + template void draw_tms_text(int start, int end); + void draw_sms(int start, int end, uint32_t cram_dot); - if(screen_mode_ == ScreenMode::Graphics) { - // If this is high resolution mode, allow the row number to affect the pattern and colour addresses. - pattern_base &= size_t(0x2000 | ((write_pointer_.row & 0xc0) << 5)); - colour_base &= size_t(0x2000 | ((write_pointer_.row & 0xc0) << 5)); + template void draw_yamaha(uint8_t y, int start, int end); + void draw_yamaha(uint8_t y, int start, int end); - colour_base += size_t(write_pointer_.row & 7); - colour_name_shift = 0; - } else { - colour_base &= size_t(0xffc0); - pattern_base &= size_t(0x3800); - } - - if(screen_mode_ == ScreenMode::MultiColour) { - pattern_base += size_t((write_pointer_.row >> 2) & 7); - } else { - pattern_base += size_t(write_pointer_.row & 7); - } - - switch(start) { - default: assert(false); - - external_slots_2(0); - - sprite_fetch_block(2, 0); - sprite_fetch_block(8, 1); - sprite_fetch_coordinates(14, 2); - - external_slots_4(16); - external_slot(20); - - sprite_fetch_graphics(21, 2); - sprite_fetch_block(25, 3); - - slot(31): - sprite_selection_buffer.reset_sprite_collection(); - do_external_slot(31*2); - external_slots_2(32); - external_slot(34); - - sprite_y_read(35, 0); - sprite_y_read(36, 1); - sprite_y_read(37, 2); - sprite_y_read(38, 3); - sprite_y_read(39, 4); - sprite_y_read(40, 5); - sprite_y_read(41, 6); - sprite_y_read(42, 7); - - background_fetch_block(43, 0, 8); - background_fetch_block(59, 4, 11); - background_fetch_block(75, 8, 14); - background_fetch_block(91, 12, 17); - background_fetch_block(107, 16, 20); - background_fetch_block(123, 20, 23); - background_fetch_block(139, 24, 26); - background_fetch_block(155, 28, 29); - - return; - } - -#undef background_fetch_block -#undef fetch_tile -#undef fetch_tile_name -#undef sprite_y_read -#undef sprite_fetch_block -#undef sprite_fetch_graphics -#undef sprite_fetch_coordinates - } - - -/*********************************************** - Master System Fetching Code -************************************************/ - - template void fetch_sms(int start, int end) { -#define sprite_fetch(sprite) {\ - line_buffer.active_sprites[sprite].x = \ - ram_[\ - master_system_.sprite_attribute_table_address & size_t(0x3f80 | (line_buffer.active_sprites[sprite].index << 1))\ - ] - (master_system_.shift_sprites_8px_left ? 8 : 0); \ - const uint8_t name = ram_[\ - master_system_.sprite_attribute_table_address & size_t(0x3f81 | (line_buffer.active_sprites[sprite].index << 1))\ - ] & (sprites_16x16_ ? ~1 : ~0);\ - const size_t graphic_location = master_system_.sprite_generator_table_address & size_t(0x2000 | (name << 5) | (line_buffer.active_sprites[sprite].row << 2)); \ - line_buffer.active_sprites[sprite].image[0] = ram_[graphic_location]; \ - line_buffer.active_sprites[sprite].image[1] = ram_[graphic_location+1]; \ - line_buffer.active_sprites[sprite].image[2] = ram_[graphic_location+2]; \ - line_buffer.active_sprites[sprite].image[3] = ram_[graphic_location+3]; \ - } - -#define sprite_fetch_block(location, sprite) \ - slot(location): \ - slot(location+1): \ - slot(location+2): \ - slot(location+3): \ - slot(location+4): \ - slot(location+5): \ - sprite_fetch(sprite);\ - sprite_fetch(sprite+1); - -#define sprite_y_read(location, sprite) \ - slot(location): \ - posit_sprite(sprite_selection_buffer, sprite, ram_[master_system_.sprite_attribute_table_address & ((sprite) | 0x3f00)], write_pointer_.row); \ - posit_sprite(sprite_selection_buffer, sprite+1, ram_[master_system_.sprite_attribute_table_address & ((sprite + 1) | 0x3f00)], write_pointer_.row); \ - -#define fetch_tile_name(column, row_info) {\ - const size_t scrolled_column = (column - horizontal_offset) & 0x1f;\ - const size_t address = row_info.pattern_address_base + (scrolled_column << 1); \ - line_buffer.names[column].flags = ram_[address+1]; \ - line_buffer.names[column].offset = size_t( \ - (((line_buffer.names[column].flags&1) << 8) | ram_[address]) << 5 \ - ) + row_info.sub_row[(line_buffer.names[column].flags&4) >> 2]; \ - } - -#define fetch_tile(column) \ - line_buffer.patterns[column][0] = ram_[line_buffer.names[column].offset]; \ - line_buffer.patterns[column][1] = ram_[line_buffer.names[column].offset+1]; \ - line_buffer.patterns[column][2] = ram_[line_buffer.names[column].offset+2]; \ - line_buffer.patterns[column][3] = ram_[line_buffer.names[column].offset+3]; - -#define background_fetch_block(location, column, sprite, row_info) \ - slot(location): fetch_tile_name(column, row_info) \ - external_slot(location+1); \ - slot(location+2): \ - slot(location+3): \ - slot(location+4): \ - fetch_tile(column) \ - fetch_tile_name(column+1, row_info) \ - sprite_y_read(location+5, sprite); \ - slot(location+6): \ - slot(location+7): \ - slot(location+8): \ - fetch_tile(column+1) \ - fetch_tile_name(column+2, row_info) \ - sprite_y_read(location+9, sprite+2); \ - slot(location+10): \ - slot(location+11): \ - slot(location+12): \ - fetch_tile(column+2) \ - fetch_tile_name(column+3, row_info) \ - sprite_y_read(location+13, sprite+4); \ - slot(location+14): \ - slot(location+15): fetch_tile(column+3) - - // Determine the coarse horizontal scrolling offset; this isn't applied on the first two lines if the programmer has requested it. - LineBuffer &line_buffer = line_buffers_[write_pointer_.row]; - LineBuffer &sprite_selection_buffer = line_buffers_[(write_pointer_.row + 1) % mode_timing_.total_lines]; - const int horizontal_offset = (write_pointer_.row >= 16 || !master_system_.horizontal_scroll_lock) ? (line_buffer.latched_horizontal_scroll >> 3) : 0; - - // Limit address bits in use if this is a SMS2 mode. - const bool is_tall_mode = mode_timing_.pixel_lines != 192; - const size_t pattern_name_address = master_system_.pattern_name_address | (is_tall_mode ? 0x800 : 0); - const size_t pattern_name_offset = is_tall_mode ? 0x100 : 0; - - // Determine row info for the screen both (i) if vertical scrolling is applied; and (ii) if it isn't. - // The programmer can opt out of applying vertical scrolling to the right-hand portion of the display. - const int scrolled_row = (write_pointer_.row + master_system_.latched_vertical_scroll) % (is_tall_mode ? 256 : 224); - struct RowInfo { - size_t pattern_address_base; - size_t sub_row[2]; - }; - const RowInfo scrolled_row_info = { - (pattern_name_address & size_t(((scrolled_row & ~7) << 3) | 0x3800)) - pattern_name_offset, - {size_t((scrolled_row & 7) << 2), 28 ^ size_t((scrolled_row & 7) << 2)} - }; - RowInfo row_info; - if(master_system_.vertical_scroll_lock) { - row_info.pattern_address_base = (pattern_name_address & size_t(((write_pointer_.row & ~7) << 3) | 0x3800)) - pattern_name_offset; - row_info.sub_row[0] = size_t((write_pointer_.row & 7) << 2); - row_info.sub_row[1] = 28 ^ size_t((write_pointer_.row & 7) << 2); - } else row_info = scrolled_row_info; - - // ... and do the actual fetching, which follows this routine: - switch(start) { - default: assert(false); - - sprite_fetch_block(0, 0); - sprite_fetch_block(6, 2); - - external_slots_4(12); - external_slot(16); - - sprite_fetch_block(17, 4); - sprite_fetch_block(23, 6); - - slot(29): - sprite_selection_buffer.reset_sprite_collection(); - do_external_slot(29*2); - external_slot(30); - - sprite_y_read(31, 0); - sprite_y_read(32, 2); - sprite_y_read(33, 4); - sprite_y_read(34, 6); - sprite_y_read(35, 8); - sprite_y_read(36, 10); - sprite_y_read(37, 12); - sprite_y_read(38, 14); - - background_fetch_block(39, 0, 16, scrolled_row_info); - background_fetch_block(55, 4, 22, scrolled_row_info); - background_fetch_block(71, 8, 28, scrolled_row_info); - background_fetch_block(87, 12, 34, scrolled_row_info); - background_fetch_block(103, 16, 40, scrolled_row_info); - background_fetch_block(119, 20, 46, scrolled_row_info); - background_fetch_block(135, 24, 52, row_info); - background_fetch_block(151, 28, 58, row_info); - - external_slots_4(167); - - return; - } - -#undef background_fetch_block -#undef fetch_tile -#undef fetch_tile_name -#undef sprite_y_read -#undef sprite_fetch_block -#undef sprite_fetch - } - -#undef external_slot -#undef slot - - uint32_t *pixel_target_ = nullptr, *pixel_origin_ = nullptr; - bool asked_for_write_area_ = false; - void draw_tms_character(int start, int end); - void draw_tms_text(int start, int end); - void draw_sms(int start, int end, uint32_t cram_dot); + template void draw_sprites(uint8_t y, int start, int end, const std::array &palette, int *colour_buffer = nullptr); }; -} } -#endif /* TMS9918Base_hpp */ +#include "Fetch.hpp" +#include "Draw.hpp" diff --git a/Components/9918/Implementation/AccessEnums.hpp b/Components/9918/Implementation/AccessEnums.hpp new file mode 100644 index 000000000..b6c104dc7 --- /dev/null +++ b/Components/9918/Implementation/AccessEnums.hpp @@ -0,0 +1,111 @@ +// +// AccessEnums.hpp +// Clock Signal +// +// Created by Thomas Harte on 26/01/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +namespace TI::TMS { + +// The screen mode is a necessary predecessor to picking the line mode, +// which is the thing latched per line. +enum class ScreenMode { + // Original TMS modes. + Blank, + Text, + MultiColour, + ColouredText, + Graphics, + + // 8-bit Sega modes. + SMSMode4, + + // New Yamaha V9938 modes. + YamahaText80, + YamahaGraphics3, + YamahaGraphics4, + YamahaGraphics5, + YamahaGraphics6, + YamahaGraphics7, + + // Rebranded Yamaha V9938 modes. + YamahaGraphics1 = ColouredText, + YamahaGraphics2 = Graphics, +}; + +constexpr int pixels_per_byte(ScreenMode mode) { + switch(mode) { + default: + case ScreenMode::Blank: return 1; + case ScreenMode::Text: return 6; + case ScreenMode::MultiColour: return 2; + case ScreenMode::ColouredText: return 8; + case ScreenMode::Graphics: return 8; + case ScreenMode::SMSMode4: return 2; + case ScreenMode::YamahaText80: return 6; + case ScreenMode::YamahaGraphics3: return 8; + case ScreenMode::YamahaGraphics4: return 2; + case ScreenMode::YamahaGraphics5: return 4; + case ScreenMode::YamahaGraphics6: return 2; + case ScreenMode::YamahaGraphics7: return 1; + } +} + +constexpr int width(ScreenMode mode) { + switch(mode) { + default: + case ScreenMode::Blank: return 0; + case ScreenMode::Text: return 240; + case ScreenMode::MultiColour: return 256; + case ScreenMode::ColouredText: return 256; + case ScreenMode::Graphics: return 256; + case ScreenMode::SMSMode4: return 256; + case ScreenMode::YamahaText80: return 480; + case ScreenMode::YamahaGraphics3: return 256; + case ScreenMode::YamahaGraphics4: return 256; + case ScreenMode::YamahaGraphics5: return 512; + case ScreenMode::YamahaGraphics6: return 512; + case ScreenMode::YamahaGraphics7: return 256; + } +} + +constexpr bool interleaves_banks(ScreenMode mode) { + return mode == ScreenMode::YamahaGraphics6 || mode == ScreenMode::YamahaGraphics7; +} + +constexpr bool is_text(ScreenMode mode) { + return mode == ScreenMode::Text || mode == ScreenMode::YamahaText80; +} + +enum class FetchMode { + Text, + Character, + Refresh, + SMS, + Yamaha, +}; + +enum class MemoryAccess { + Read, Write, None +}; + +enum class VerticalState { + /// Describes any line on which pixels do not appear and no fetching occurs, including + /// the border, blanking and sync. + Blank, + /// A line on which pixels do not appear but fetching occurs. + Prefetch, + /// A line on which pixels appear and fetching occurs. + Pixels, +}; + +enum class SpriteMode { + Mode1, + Mode2, + MasterSystem, +}; + +} diff --git a/Components/9918/Implementation/ClockConverter.hpp b/Components/9918/Implementation/ClockConverter.hpp new file mode 100644 index 000000000..9d7699749 --- /dev/null +++ b/Components/9918/Implementation/ClockConverter.hpp @@ -0,0 +1,165 @@ +// +// ClockConverter.hpp +// Clock Signal +// +// Created by Thomas Harte on 01/01/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "../9918.hpp" +#include "PersonalityTraits.hpp" +#include "LineLayout.hpp" + +namespace TI::TMS { + +enum class Clock { + /// Whatever rate this VDP runs at, with location 0 being "the start" of the line per internal preference. + Internal, + /// A 342-cycle/line clock with the same start position as ::Internal. + TMSPixel, + /// A 171-cycle/line clock that begins at the memory window which starts straight after ::Internal = 0. + TMSMemoryWindow, + /// A fixed 1368-cycle/line clock that is used to count output to the CRT. + CRT, + /// Provides the same clock rate as ::Internal but is relocated so that 0 is the start of horizontal sync — very not coincidentally, + /// where Grauw puts 0 on his detailed TMS and Yamaha timing diagrams. + FromStartOfSync, +}; + +template constexpr int clock_rate() { + static_assert( + is_classic_vdp(personality) || + is_yamaha_vdp(personality) || + (personality == Personality::MDVDP) + ); + + switch(clk) { + case Clock::TMSPixel: return 342; + case Clock::TMSMemoryWindow: return 171; + case Clock::CRT: return 1368; + case Clock::Internal: + case Clock::FromStartOfSync: + if constexpr (is_classic_vdp(personality)) { + return 342; + } else if constexpr (is_yamaha_vdp(personality)) { + return 1368; + } else if constexpr (personality == Personality::MDVDP) { + return 3420; + } + } +} + +/// Statelessly converts @c length to the internal clock for @c personality; applies conversions per the list of clocks in left-to-right order. +template constexpr int to_internal(int length) { + if constexpr (head == Clock::FromStartOfSync) { + length = (length + LineLayout::StartOfSync) % LineLayout::CyclesPerLine; + } else { + length = length * clock_rate() / clock_rate(); + } + + if constexpr (!sizeof...(tail)) { + return length; + } else { + return to_internal(length); + } +} + +/// Statelessly converts @c length to @c clock from the the internal clock used by VDPs of @c personality throwing away any remainder. +template constexpr int from_internal(int length) { + if constexpr (head == Clock::FromStartOfSync) { + length = + (length + LineLayout::CyclesPerLine - LineLayout::StartOfSync) + % LineLayout::CyclesPerLine; + } else { + length = length * clock_rate() / clock_rate(); + } + + if constexpr (!sizeof...(tail)) { + return length; + } else { + return to_internal(length); + } +} + +/*! + Provides a [potentially-]stateful conversion between the external and internal clocks. + Unlike the other clock conversions, this may be non-integral, requiring that + an error term be tracked. +*/ +template class ClockConverter { + public: + /*! + Given that another @c source external **half-cycles** has occurred, + indicates how many complete internal **cycles** have additionally elapsed + since the last call to @c to_internal. + + E.g. for the TMS, @c source will count 456 ticks per line, and the internal clock + runs at 342 ticks per line, so the proper conversion is to multiply by 3/4. + */ + int to_internal(int source) { + switch(personality) { + // Default behaviour is to apply a multiplication by 3/4; + // this is correct for the TMS and Sega VDPs other than the Mega Drive. + default: { + const int result = source * 3 + cycles_error_; + cycles_error_ = result & 3; + return result >> 2; + } + + // The two Yamaha chips have an internal clock that is four times + // as fast as the TMS, therefore a stateless translation is possible. + case Personality::V9938: + case Personality::V9958: + return source * 3; + + // The Mega Drive runs at 3420 master clocks per line, which is then + // divided by 4 or 5 depending on other state. That's 7 times the + // rate provided to the CPU; given that the input is in half-cycles + // the proper multiplier is therefore 3.5. + case Personality::MDVDP: { + const int result = source * 7 + cycles_error_; + cycles_error_ = result & 1; + return result >> 1; + } + } + } + + /*! + Provides the number of external cycles that need to begin from now in order to + get at least @c internal_cycles into the future. + */ + HalfCycles half_cycles_before_internal_cycles(int internal_cycles) const { + // Logic here correlates with multipliers as per @c to_internal. + switch(personality) { + default: + // Relative to the external clock multiplied by 3, it will definitely take this + // many cycles to complete a further (internal_cycles - 1) after the current one. + internal_cycles = (internal_cycles - 1) << 2; + + // It will also be necessary to complete the current one. + internal_cycles += 4 - cycles_error_; + + // Round up to get the first external cycle after + // the number of internal_cycles has elapsed. + return HalfCycles((internal_cycles + 2) / 3); + + case Personality::V9938: + case Personality::V9958: + return HalfCycles((internal_cycles + 2) / 3); + + case Personality::MDVDP: + internal_cycles = (internal_cycles - 1) << 1; + internal_cycles += 2 - cycles_error_; + return HalfCycles((internal_cycles + 6) / 7); + } + } + + private: + // Holds current residue in conversion from the external to + // internal clock. + int cycles_error_ = 0; +}; + +} diff --git a/Components/9918/Implementation/Draw.hpp b/Components/9918/Implementation/Draw.hpp new file mode 100644 index 000000000..7204e8b28 --- /dev/null +++ b/Components/9918/Implementation/Draw.hpp @@ -0,0 +1,569 @@ +// +// Draw.hpp +// Clock Signal +// +// Created by Thomas Harte on 05/01/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +namespace TI::TMS { + +// MARK: - Sprites, as generalised. + +template +template +void Base::draw_sprites([[maybe_unused]] uint8_t y, int start, int end, const std::array &palette, int *colour_buffer) { + if(!draw_line_buffer_->sprites) { + return; + } + + auto &buffer = *draw_line_buffer_->sprites; + if(!buffer.active_sprite_slot) { + return; + } + + const int shift_advance = sprites_magnified_ ? 1 : 2; + + // If this is the start of the line clip any part of any sprites that is off to the left. + if(!start) { + for(int index = 0; index < buffer.active_sprite_slot; ++index) { + auto &sprite = buffer.active_sprites[index]; + if(sprite.x < 0) sprite.shift_position -= shift_advance * sprite.x; + } + } + + int sprite_buffer[256]; + int sprite_collision = 0; + memset(&sprite_buffer[start], 0, size_t(end - start)*sizeof(sprite_buffer[0])); + + if constexpr (mode == SpriteMode::MasterSystem) { + // Draw all sprites into the sprite buffer. + for(int index = buffer.active_sprite_slot - 1; index >= 0; --index) { + auto &sprite = buffer.active_sprites[index]; + if(sprite.shift_position >= 16) { + continue; + } + + const int pixel_start = std::max(start, sprite.x); + + // TODO: it feels like the work below should be simplifiable; + // the double shift in particular, and hopefully the variable shift. + for(int c = pixel_start; c < end && sprite.shift_position < 16; ++c) { + const int shift = (sprite.shift_position >> 1); + const int sprite_colour = + (((sprite.image[3] << shift) & 0x80) >> 4) | + (((sprite.image[2] << shift) & 0x80) >> 5) | + (((sprite.image[1] << shift) & 0x80) >> 6) | + (((sprite.image[0] << shift) & 0x80) >> 7); + + if(sprite_colour) { + sprite_collision |= sprite_buffer[c]; + sprite_buffer[c] = sprite_colour | 0x10; + } + + sprite.shift_position += shift_advance; + } + } + + // Draw the sprite buffer onto the colour buffer, wherever the tile map doesn't have + // priority (or is transparent). + for(int c = start; c < end; ++c) { + if( + sprite_buffer[c] && + (!(colour_buffer[c]&0x20) || !(colour_buffer[c]&0xf)) + ) colour_buffer[c] = sprite_buffer[c]; + } + + if(sprite_collision) { + status_ |= StatusSpriteCollision; + } + + return; + } + + if constexpr (SpriteBuffer::test_is_filling) { + assert(!buffer.is_filling); + } + + constexpr uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff}; + constexpr int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; + const int sprite_width = sprites_16x16_ ? 16 : 8; + const int shifter_target = sprite_width << 1; + const int pixel_width = sprites_magnified_ ? sprite_width << 1 : sprite_width; + int min_sprite = 0; + + // + // Approach taken for Mode 2 sprites: + // + // (1) precompute full sprite images, at up to 32 pixels wide; + // (2) for each sprite that is marked as CC, walk backwards until the + // first sprite that is not marked CC, ORing it into the precomputed + // image at each step; + // (3) subsequently, just draw each sprite image independently. + // + if constexpr (mode == SpriteMode::Mode2) { + // Determine the lowest visible sprite; exit early if that leaves no sprites visible. + for(; min_sprite < buffer.active_sprite_slot; min_sprite++) { + auto &sprite = buffer.active_sprites[min_sprite]; + if(sprite.opaque()) { + break; + } + } + if(min_sprite == buffer.active_sprite_slot) { + return; + } + + if(!start) { + // Pre-rasterise the sprites one-by-one. + if(sprites_magnified_) { + for(int index = min_sprite; index < buffer.active_sprite_slot; index++) { + auto &sprite = buffer.active_sprites[index]; + for(int c = 0; c < 32; c+= 2) { + const int shift = (c >> 1) ^ 7; + const int bit = 1 & (sprite.image[shift >> 3] >> (shift & 7)); + + Storage::sprite_cache_[index][c] = + Storage::sprite_cache_[index][c + 1] = + (sprite.image[2] & 0xf & sprite_colour_selection_masks[bit]) | + uint8_t((bit << StatusSpriteCollisionShift) & sprite.collision_bit()); + } + } + } else { + for(int index = min_sprite; index < buffer.active_sprite_slot; index++) { + auto &sprite = buffer.active_sprites[index]; + for(int c = 0; c < 16; c++) { + const int shift = c ^ 7; + const int bit = 1 & (sprite.image[shift >> 3] >> (shift & 7)); + + Storage::sprite_cache_[index][c] = + (sprite.image[2] & 0xf & sprite_colour_selection_masks[bit]) | + uint8_t((bit << StatusSpriteCollisionShift) & sprite.collision_bit()); + } + } + } + + // Go backwards compositing any sprites that are set as OR masks onto their parents. + for(int index = buffer.active_sprite_slot - 1; index >= min_sprite + 1; --index) { + auto &sprite = buffer.active_sprites[index]; + if(sprite.opaque()) { + continue; + } + + // Sprite may affect all previous up to and cindlugin the next one that is opaque. + for(int previous_index = index - 1; previous_index >= min_sprite; --previous_index) { + // Determine region of overlap (if any). + auto &previous = buffer.active_sprites[previous_index]; + const int origin = sprite.x - previous.x; + const int x1 = std::max(0, -origin); + const int x2 = std::min(pixel_width - origin, pixel_width); + + // Composite sprites. + for(int x = x1; x < x2; x++) { + Storage::sprite_cache_[previous_index][x + origin] + |= Storage::sprite_cache_[index][x]; + } + + // If a previous opaque sprite has been found, stop. + if(previous.opaque()) { + break; + } + } + } + } + + // Draw. + for(int index = buffer.active_sprite_slot - 1; index >= min_sprite; --index) { + auto &sprite = buffer.active_sprites[index]; + const int x1 = std::max(0, start - sprite.x); + const int x2 = std::min(end - sprite.x, pixel_width); + + for(int x = x1; x < x2; x++) { + const uint8_t colour = Storage::sprite_cache_[index][x]; + + // Plot colour, if visible. + if(colour) { + pixel_origin_[sprite.x + x] = palette[colour & 0xf]; + } + + // TODO: is collision location recorded in mode 1? + + // Check for a new collision. + if(!(status_ & StatusSpriteCollision)) { + sprite_collision |= sprite_buffer[sprite.x + x]; + sprite_buffer[sprite.x + x] |= colour; + status_ |= sprite_collision & StatusSpriteCollision; + + if(status_ & StatusSpriteCollision) { + Storage::collision_location_[0] = uint16_t(x); + Storage::collision_location_[1] = uint16_t(y); + } + } + } + } + + return; + } + + if constexpr (mode == SpriteMode::Mode1) { + for(int index = buffer.active_sprite_slot - 1; index >= min_sprite; --index) { + auto &sprite = buffer.active_sprites[index]; + if(sprite.shift_position >= shifter_target) { + continue; + } + + const int pixel_start = std::max(start, sprite.x); + for(int c = pixel_start; c < end && sprite.shift_position < shifter_target; ++c) { + const int shift = (sprite.shift_position >> 1) ^ 7; + int sprite_colour = (sprite.image[shift >> 3] >> (shift & 7)) & 1; + + // A colision is detected regardless of sprite colour ... + sprite_collision |= sprite_buffer[c] & sprite_colour; + sprite_buffer[c] |= sprite_colour; + + // ... but a sprite with the transparent colour won't actually be visible. + sprite_colour &= colour_masks[sprite.image[2] & 0xf]; + + pixel_origin_[c] = + (pixel_origin_[c] & sprite_colour_selection_masks[sprite_colour^1]) | + (palette[sprite.image[2] & 0xf] & sprite_colour_selection_masks[sprite_colour]); + + sprite.shift_position += shift_advance; + } + } + + status_ |= sprite_collision << StatusSpriteCollisionShift; + return; + } +} + +// Mode 2 logic, as I currently understand it, as a note for my future self: +// +// If a sprite is marked as 'CC' then it doesn't collide, but its colour value is +// ORd with those of all lower-numbered sprites down to the next one that is visible on +// that line and not marked CC. +// +// If no previous sprite meets that criteria, no pixels are displayed. But if one does +// then pixels are displayed even where they don't overlap with the earlier sprites. +// +// ... so in terms of my loop above, I guess I need temporary storage to accumulate +// an OR mask up until I hit a non-CC sprite, at which point I composite everything out? +// I'm not immediately sure whether I can appropriately reuse sprite_buffer, but possibly? + +// MARK: - TMS9918 + +template +template +void Base::draw_tms_character(int start, int end) { + auto &line_buffer = *draw_line_buffer_; + + // Paint the background tiles. + const int pixels_left = end - start; + if(this->screen_mode_ == ScreenMode::MultiColour) { + for(int c = start; c < end; ++c) { + pixel_target_[c] = palette()[ + (line_buffer.tiles.patterns[c >> 3][0] >> (((c & 4)^4))) & 15 + ]; + } + } else { + const int shift = start & 7; + int byte_column = start >> 3; + + int length = std::min(pixels_left, 8 - shift); + + int pattern = Numeric::bit_reverse(line_buffer.tiles.patterns[byte_column][0]) >> shift; + uint8_t colour = line_buffer.tiles.patterns[byte_column][1]; + uint32_t colours[2] = { + palette()[(colour & 15) ? (colour & 15) : background_colour_], + palette()[(colour >> 4) ? (colour >> 4) : background_colour_] + }; + + int background_pixels_left = pixels_left; + while(true) { + background_pixels_left -= length; + for(int c = 0; c < length; ++c) { + pixel_target_[c] = colours[pattern&0x01]; + pattern >>= 1; + } + pixel_target_ += length; + + if(!background_pixels_left) break; + length = std::min(8, background_pixels_left); + byte_column++; + + pattern = Numeric::bit_reverse(line_buffer.tiles.patterns[byte_column][0]); + colour = line_buffer.tiles.patterns[byte_column][1]; + colours[0] = palette()[(colour & 15) ? (colour & 15) : background_colour_]; + colours[1] = palette()[(colour >> 4) ? (colour >> 4) : background_colour_]; + } + } + + draw_sprites(0, start, end, palette()); // TODO: propagate a real 'y' into here. +} + +template +template +void Base::draw_tms_text(int start, int end) { + auto &line_buffer = *draw_line_buffer_; + uint32_t colours[2][2] = { + {palette()[background_colour_], palette()[text_colour_]}, + {0, 0} + }; + if constexpr (apply_blink) { + colours[1][0] = palette()[Storage::blink_background_colour_]; + colours[1][1] = palette()[Storage::blink_text_colour_]; + } + + const int shift = start % 6; + int byte_column = start / 6; + int pattern = Numeric::bit_reverse(line_buffer.characters.shapes[byte_column]) >> shift; + int pixels_left = end - start; + int length = std::min(pixels_left, 6 - shift); + int flag = 0; + if constexpr (apply_blink) { + flag = (line_buffer.characters.flags[byte_column >> 3] >> ((byte_column & 7) ^ 7)) & Storage::in_blink_; + } + while(true) { + pixels_left -= length; + for(int c = 0; c < length; ++c) { + pixel_target_[c] = colours[flag][(pattern&0x01)]; + pattern >>= 1; + } + pixel_target_ += length; + + if(!pixels_left) break; + length = std::min(6, pixels_left); + byte_column++; + pattern = Numeric::bit_reverse(line_buffer.characters.shapes[byte_column]); + if constexpr (apply_blink) { + flag = (line_buffer.characters.flags[byte_column >> 3] >> ((byte_column & 7) ^ 7)) & Storage::in_blink_; + } + } +} + +// MARK: - Master System + +template +void Base::draw_sms([[maybe_unused]] int start, [[maybe_unused]] int end, [[maybe_unused]] uint32_t cram_dot) { + if constexpr (is_sega_vdp(personality)) { + int colour_buffer[256]; + auto &line_buffer = *draw_line_buffer_; + + /* + Add extra border for any pixels that fall before the fine scroll. + */ + int tile_start = start, tile_end = end; + int tile_offset = start; + if(output_pointer_.row >= 16 || !Storage::horizontal_scroll_lock_) { + for(int c = start; c < (line_buffer.latched_horizontal_scroll & 7); ++c) { + colour_buffer[c] = 16 + background_colour_; + ++tile_offset; + } + + // Remove the border area from that to which tiles will be drawn. + tile_start = std::max(start - (line_buffer.latched_horizontal_scroll & 7), 0); + tile_end = std::max(end - (line_buffer.latched_horizontal_scroll & 7), 0); + } + + + uint32_t pattern; + uint8_t *const pattern_index = reinterpret_cast(&pattern); + + /* + Add background tiles; these will fill the colour_buffer with values in which + the low five bits are a palette index, and bit six is set if this tile has + priority over sprites. + */ + if(tile_start < end) { + const int shift = tile_start & 7; + int byte_column = tile_start >> 3; + int pixels_left = tile_end - tile_start; + int length = std::min(pixels_left, 8 - shift); + + pattern = *reinterpret_cast(line_buffer.tiles.patterns[byte_column]); + if(line_buffer.tiles.flags[byte_column]&2) + pattern >>= shift; + else + pattern <<= shift; + + while(true) { + const int palette_offset = (line_buffer.tiles.flags[byte_column]&0x18) << 1; + if(line_buffer.tiles.flags[byte_column]&2) { + for(int c = 0; c < length; ++c) { + colour_buffer[tile_offset] = + ((pattern_index[3] & 0x01) << 3) | + ((pattern_index[2] & 0x01) << 2) | + ((pattern_index[1] & 0x01) << 1) | + ((pattern_index[0] & 0x01) << 0) | + palette_offset; + ++tile_offset; + pattern >>= 1; + } + } else { + for(int c = 0; c < length; ++c) { + colour_buffer[tile_offset] = + ((pattern_index[3] & 0x80) >> 4) | + ((pattern_index[2] & 0x80) >> 5) | + ((pattern_index[1] & 0x80) >> 6) | + ((pattern_index[0] & 0x80) >> 7) | + palette_offset; + ++tile_offset; + pattern <<= 1; + } + } + + pixels_left -= length; + if(!pixels_left) break; + + length = std::min(8, pixels_left); + byte_column++; + pattern = *reinterpret_cast(line_buffer.tiles.patterns[byte_column]); + } + } + + /* + Apply sprites (if any). + */ + draw_sprites(0, start, end, palette(), colour_buffer); // TODO provide good y, as per elsewhere. + + // Map from the 32-colour buffer to real output pixels, applying the specific CRAM dot if any. + pixel_target_[start] = Storage::colour_ram_[colour_buffer[start] & 0x1f] | cram_dot; + for(int c = start+1; c < end; ++c) { + pixel_target_[c] = Storage::colour_ram_[colour_buffer[c] & 0x1f]; + } + + // If the VDP is set to hide the left column and this is the final call that'll come + // this line, hide it. + if(end == 256) { + if(Storage::hide_left_column_) { + pixel_origin_[0] = pixel_origin_[1] = pixel_origin_[2] = pixel_origin_[3] = + pixel_origin_[4] = pixel_origin_[5] = pixel_origin_[6] = pixel_origin_[7] = + Storage::colour_ram_[16 + background_colour_]; + } + } + } +} + +// MARK: - Yamaha + +template +template +void Base::draw_yamaha(uint8_t y, int start, int end) { + [[maybe_unused]] const auto active_palette = palette(); + const int sprite_start = start >> 2; + const int sprite_end = end >> 2; + auto &line_buffer = *draw_line_buffer_; + + // Observation justifying Duff's device below: it's acceptable to paint too many pixels — to paint + // beyond `end` — provided that the overpainting is within normal bitmap bounds, because any + // mispainted pixels will be replaced before becoming visible to the user. + + if constexpr (mode == ScreenMode::YamahaGraphics4 || mode == ScreenMode::YamahaGraphics6) { + start >>= (mode == ScreenMode::YamahaGraphics4) ? 2 : 1; + end >>= (mode == ScreenMode::YamahaGraphics4) ? 2 : 1; + + int column = start & ~1; + const int offset = start & 1; + start >>= 1; + end = (end + 1) >> 1; + + switch(offset) { + case 0: + do { + pixel_target_[column+0] = active_palette[line_buffer.bitmap[start] >> 4]; [[fallthrough]]; + case 1: pixel_target_[column+1] = active_palette[line_buffer.bitmap[start] & 0xf]; + ++start; + column += 2; + } while(start < end); + } + } + + if constexpr (mode == ScreenMode::YamahaGraphics5) { + start >>= 1; + end >>= 1; + + int column = start & ~3; + const int offset = start & 3; + start >>= 2; + end = (end + 3) >> 2; + + switch(offset) { + case 0: + do { + pixel_target_[column+0] = active_palette[line_buffer.bitmap[start] >> 6]; [[fallthrough]]; + case 1: pixel_target_[column+1] = active_palette[(line_buffer.bitmap[start] >> 4) & 3]; [[fallthrough]]; + case 2: pixel_target_[column+2] = active_palette[(line_buffer.bitmap[start] >> 2) & 3]; [[fallthrough]]; + case 3: pixel_target_[column+3] = active_palette[line_buffer.bitmap[start] & 3]; + ++start; + column += 4; + } while(start < end); + } + } + + if constexpr (mode == ScreenMode::YamahaGraphics7) { + start >>= 2; + end >>= 2; + + while(start < end) { + pixel_target_[start] = + palette_pack( + uint8_t((line_buffer.bitmap[start] & 0x1c) + ((line_buffer.bitmap[start] & 0x1c) << 3) + ((line_buffer.bitmap[start] & 0x1c) >> 3)), + uint8_t((line_buffer.bitmap[start] & 0xe0) + ((line_buffer.bitmap[start] & 0xe0) >> 3) + ((line_buffer.bitmap[start] & 0xe0) >> 6)), + uint8_t((line_buffer.bitmap[start] & 0x03) + ((line_buffer.bitmap[start] & 0x03) << 2) + ((line_buffer.bitmap[start] & 0x03) << 4) + ((line_buffer.bitmap[start] & 0x03) << 6)) + ); + ++start; + } + } + + constexpr std::array graphics7_sprite_palette = { + palette_pack(0b00000000, 0b00000000, 0b00000000), palette_pack(0b00000000, 0b00000000, 0b01001001), + palette_pack(0b00000000, 0b01101101, 0b00000000), palette_pack(0b00000000, 0b01101101, 0b01001001), + palette_pack(0b01101101, 0b00000000, 0b00000000), palette_pack(0b01101101, 0b00000000, 0b01001001), + palette_pack(0b01101101, 0b01101101, 0b00000000), palette_pack(0b01101101, 0b01101101, 0b01001001), + + palette_pack(0b10010010, 0b11111111, 0b01001001), palette_pack(0b00000000, 0b00000000, 0b11111111), + palette_pack(0b00000000, 0b11111111, 0b00000000), palette_pack(0b00000000, 0b11111111, 0b11111111), + palette_pack(0b11111111, 0b00000000, 0b00000000), palette_pack(0b11111111, 0b00000000, 0b11111111), + palette_pack(0b11111111, 0b11111111, 0b00000000), palette_pack(0b11111111, 0b11111111, 0b11111111), + }; + + // Possibly TODO: is the data-sheet trying to allege some sort of colour mixing for sprites in Mode 6? + draw_sprites< + SpriteMode::Mode2, + mode == ScreenMode::YamahaGraphics5 || mode == ScreenMode::YamahaGraphics6 + >(y, sprite_start, sprite_end, mode == ScreenMode::YamahaGraphics7 ? graphics7_sprite_palette : palette()); +} + +template +void Base::draw_yamaha(uint8_t y, int start, int end) { + if constexpr (is_yamaha_vdp(personality)) { + switch(draw_line_buffer_->screen_mode) { + // Modes that are the same (or close enough) to those on the TMS. + case ScreenMode::Text: draw_tms_text(start >> 2, end >> 2); break; + case ScreenMode::YamahaText80: draw_tms_text(start >> 1, end >> 1); break; + case ScreenMode::MultiColour: + case ScreenMode::ColouredText: + case ScreenMode::Graphics: draw_tms_character(start >> 2, end >> 2); break; + + case ScreenMode::YamahaGraphics3: + draw_tms_character(start >> 2, end >> 2); + break; + +#define Dispatch(x) case ScreenMode::x: draw_yamaha(y, start, end); break; + Dispatch(YamahaGraphics4); + Dispatch(YamahaGraphics5); + Dispatch(YamahaGraphics6); + Dispatch(YamahaGraphics7); +#undef Dispatch + + default: break; + } + } +} + +// MARK: - Mega Drive + +// TODO. + +} diff --git a/Components/9918/Implementation/Fetch.hpp b/Components/9918/Implementation/Fetch.hpp new file mode 100644 index 000000000..5d226bd1b --- /dev/null +++ b/Components/9918/Implementation/Fetch.hpp @@ -0,0 +1,793 @@ +// +// Fetch.hpp +// Clock Signal +// +// Created by Thomas Harte on 01/01/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +namespace TI::TMS { + +/* + Fetching routines follow below; they obey the following rules: + + 1) input is a start position and an end position; they should perform the proper + operations for the period: start <= time < end. + 2) times are measured relative to the an appropriate clock — they directly + count access windows on the TMS and Master System, and cycles on a Yamaha. + 3) within each sequencer, cycle are numbered as per Grauw's timing diagrams. The difference + between those and internal timing, if there is one, is handled by the dispatcher. + 4) all of these functions are templated with a `use_end` parameter. That will be true if + end is < [cycles per line], false otherwise. So functions can use it to eliminate + should-exit-now checks (which is likely to be the more usual path of execution). + + Provided for the benefit of the methods below: + + * the function external_slot(), which will perform any pending VRAM read/write. + + All functions should just spool data to intermediary storage. Fetching and drawing are decoupled. +*/ + +// MARK: - Address mask helpers. + +/// @returns An instance of @c AddressT with all top bits set down to and including +/// bit @c end and all others clear. +/// +/// So e.g. if @c AddressT is @c uint16_t and this VDP has a 15-bit address space then +/// @c top_bits<10> will be the address with bits 15 to 10 (inclusive) set and the rest clear. +template constexpr AddressT top_bits() { + return AddressT(~0) - AddressT((1 << end) - 1); +} + +/// Modifies and returns @c source so that all bits above position @c n are set; the others are unmodified. +template constexpr AddressT bits(AddressT source = 0) { + return AddressT(source | top_bits()); +} + +// MARK: - 171-window Dispatcher. + +template +template void Base::dispatch(SequencerT &fetcher, int start, int end) { +#define index(n) \ + if(use_end && end == n) return; \ + [[fallthrough]]; \ + case n: fetcher.template fetch(n)>(); + + switch(start) { + default: assert(false); + index(0); index(1); index(2); index(3); index(4); index(5); index(6); index(7); index(8); index(9); + index(10); index(11); index(12); index(13); index(14); index(15); index(16); index(17); index(18); index(19); + index(20); index(21); index(22); index(23); index(24); index(25); index(26); index(27); index(28); index(29); + index(30); index(31); index(32); index(33); index(34); index(35); index(36); index(37); index(38); index(39); + index(40); index(41); index(42); index(43); index(44); index(45); index(46); index(47); index(48); index(49); + index(50); index(51); index(52); index(53); index(54); index(55); index(56); index(57); index(58); index(59); + index(60); index(61); index(62); index(63); index(64); index(65); index(66); index(67); index(68); index(69); + index(70); index(71); index(72); index(73); index(74); index(75); index(76); index(77); index(78); index(79); + index(80); index(81); index(82); index(83); index(84); index(85); index(86); index(87); index(88); index(89); + index(90); index(91); index(92); index(93); index(94); index(95); index(96); index(97); index(98); index(99); + index(100); index(101); index(102); index(103); index(104); index(105); index(106); index(107); index(108); index(109); + index(110); index(111); index(112); index(113); index(114); index(115); index(116); index(117); index(118); index(119); + index(120); index(121); index(122); index(123); index(124); index(125); index(126); index(127); index(128); index(129); + index(130); index(131); index(132); index(133); index(134); index(135); index(136); index(137); index(138); index(139); + index(140); index(141); index(142); index(143); index(144); index(145); index(146); index(147); index(148); index(149); + index(150); index(151); index(152); index(153); index(154); index(155); index(156); index(157); index(158); index(159); + index(160); index(161); index(162); index(163); index(164); index(165); index(166); index(167); index(168); index(169); + index(170); + } + +#undef index +} + +// MARK: - Fetchers. + +template +struct TextFetcher { + using AddressT = typename Base::AddressT; + + TextFetcher(Base *base, uint8_t y) : + base(base), + row_base(base->pattern_name_address_ & bits<10>(AddressT((y >> 3) * 40))), + row_offset(base->pattern_generator_table_address_ & bits<11>(AddressT(y & 7))) {} + + void fetch_name(AddressT column, int slot = 0) { + base->name_[slot] = base->ram_[row_base + column]; + } + + void fetch_pattern(AddressT column, int slot = 0) { + base->fetch_line_buffer_->characters.shapes[column] = base->ram_[row_offset + size_t(base->name_[slot] << 3)]; + } + + Base *const base; + const AddressT row_base; + const AddressT row_offset; +}; + +template +struct CharacterFetcher { + using AddressT = typename Base::AddressT; + + CharacterFetcher(Base *base, uint8_t y) : + base(base), + y(y), + row_base(base->pattern_name_address_ & bits<10>(AddressT((y << 2)&~31))) + { + pattern_base = base->pattern_generator_table_address_; + colour_base = base->colour_table_address_; + colour_name_shift = 6; + + const ScreenMode mode = base->fetch_line_buffer_->screen_mode; + if(mode == ScreenMode::Graphics || mode == ScreenMode::YamahaGraphics3) { + // If this is high resolution mode, allow the row number to affect the pattern and colour addresses. + pattern_base &= bits<13>(AddressT(((y & 0xc0) << 5))); + colour_base &= bits<13>(AddressT(((y & 0xc0) << 5))); + + colour_base += AddressT(y & 7); + colour_name_shift = 0; + } else { + colour_base &= bits<6, AddressT>(); + pattern_base &= bits<11, AddressT>(); + } + + if(mode == ScreenMode::MultiColour) { + pattern_base += AddressT((y >> 2) & 7); + } else { + pattern_base += AddressT(y & 7); + } + } + + void fetch_name(int column) { + base->tile_offset_ = base->ram_[row_base + AddressT(column)]; + } + + void fetch_pattern(int column) { + base->fetch_line_buffer_->tiles.patterns[column][0] = base->ram_[pattern_base + AddressT(base->tile_offset_ << 3)]; + } + + void fetch_colour(int column) { + base->fetch_line_buffer_->tiles.patterns[column][1] = base->ram_[colour_base + AddressT((base->tile_offset_ << 3) >> colour_name_shift)]; + } + + Base *const base; + const uint8_t y; + const AddressT row_base; + AddressT pattern_base; + AddressT colour_base; + int colour_name_shift; +}; + +constexpr SpriteMode sprite_mode(ScreenMode screen_mode) { + switch(screen_mode) { + default: + return SpriteMode::Mode2; + + case ScreenMode::MultiColour: + case ScreenMode::ColouredText: + case ScreenMode::Graphics: + return SpriteMode::Mode1; + + case ScreenMode::SMSMode4: + return SpriteMode::MasterSystem; + } +} + +// TODO: should this be extended to include Master System sprites? +template +class SpriteFetcher { + public: + using AddressT = typename Base::AddressT; + + // The Yamaha VDP adds an additional table when in Sprite Mode 2, the sprite colour + // table, which is intended to fill the 512 bytes before the programmer-located sprite + // attribute table. + // + // It partially enforces this proximity by forcing bits 7 and 8 to 0 in the address of + // the attribute table, and forcing them to 1 but masking out bit 9 for the colour table. + // + // AttributeAddressMask is used to enable or disable that behaviour. + static constexpr AddressT AttributeAddressMask = (mode == SpriteMode::Mode2) ? AddressT(~0x180) : AddressT(~0x000); + + SpriteFetcher(Base *base, uint8_t y) : + base(base), + y(y) {} + + void fetch_location(int slot) { + fetch_xy(slot); + + if constexpr (mode == SpriteMode::Mode2) { + fetch_xy(slot + 1); + + base->name_[0] = name(slot); + base->name_[1] = name(slot + 1); + } + } + + void fetch_pattern(int slot) { + switch(mode) { + case SpriteMode::Mode1: + fetch_image(slot, name(slot)); + break; + + case SpriteMode::Mode2: + fetch_image(slot, base->name_[0]); + fetch_image(slot + 1, base->name_[1]); + break; + } + } + + void fetch_y(int sprite) { + const AddressT address = base->sprite_attribute_table_address_ & AttributeAddressMask & bits<7>(AddressT(sprite << 2)); + const uint8_t sprite_y = base->ram_[address]; + base->posit_sprite(sprite, sprite_y, y); + } + + private: + void fetch_xy(int slot) { + auto &buffer = *base->fetch_sprite_buffer_; + buffer.active_sprites[slot].x = + base->ram_[ + base->sprite_attribute_table_address_ & AttributeAddressMask & bits<7>(AddressT((buffer.active_sprites[slot].index << 2) | 1)) + ]; + } + + uint8_t name(int slot) { + auto &buffer = *base->fetch_sprite_buffer_; + const AddressT address = + base->sprite_attribute_table_address_ & + AttributeAddressMask & + bits<7>(AddressT((buffer.active_sprites[slot].index << 2) | 2)); + const uint8_t name = base->ram_[address] & (base->sprites_16x16_ ? ~3 : ~0); + return name; + } + + void fetch_image(int slot, uint8_t name) { + uint8_t colour = 0; + auto &sprite = base->fetch_sprite_buffer_->active_sprites[slot]; + switch(mode) { + case SpriteMode::Mode1: + // Fetch colour from the attribute table, per this sprite's slot. + colour = base->ram_[ + base->sprite_attribute_table_address_ & bits<7>(AddressT((sprite.index << 2) | 3)) + ]; + break; + + case SpriteMode::Mode2: { + // Fetch colour from the colour table, per this sprite's slot and row. + const AddressT colour_table_address = (base->sprite_attribute_table_address_ | ~AttributeAddressMask) & AddressT(~0x200); + colour = base->ram_[ + colour_table_address & + bits<9>( + AddressT(sprite.index << 4) | + AddressT(sprite.row) + ) + ]; + } break; + } + sprite.image[2] = colour; + sprite.x -= sprite.early_clock(); + + const AddressT graphic_location = base->sprite_generator_table_address_ & bits<11>(AddressT((name << 3) | sprite.row)); + sprite.image[0] = base->ram_[graphic_location]; + sprite.image[1] = base->ram_[graphic_location+16]; + + if constexpr (SpriteBuffer::test_is_filling) { + if(slot == ((mode == SpriteMode::Mode2) ? 7 : 3)) { + base->fetch_sprite_buffer_->is_filling = false; + } + } + } + + Base *const base; + const uint8_t y; +}; + +template +struct SMSFetcher { + using AddressT = typename Base::AddressT; + struct RowInfo { + AddressT pattern_address_base; + AddressT sub_row[2]; + }; + + SMSFetcher(Base *base, uint8_t y) : + base(base), + storage(static_cast *>(base)), + y(y), + horizontal_offset((y >= 16 || !storage->horizontal_scroll_lock_) ? (base->fetch_line_buffer_->latched_horizontal_scroll >> 3) : 0) + { + // Limit address bits in use if this is a SMS2 mode. + const bool is_tall_mode = base->mode_timing_.pixel_lines != 192; + const AddressT pattern_name_address = storage->pattern_name_address_ | (is_tall_mode ? 0x800 : 0); + const AddressT pattern_name_offset = is_tall_mode ? 0x100 : 0; + + // Determine row info for the screen both (i) if vertical scrolling is applied; and (ii) if it isn't. + // The programmer can opt out of applying vertical scrolling to the right-hand portion of the display. + const int scrolled_row = (y + storage->latched_vertical_scroll_) % (is_tall_mode ? 256 : 224); + scrolled_row_info.pattern_address_base = (pattern_name_address & bits<11>(AddressT((scrolled_row & ~7) << 3))) - pattern_name_offset; + scrolled_row_info.sub_row[0] = AddressT((scrolled_row & 7) << 2); + scrolled_row_info.sub_row[1] = AddressT(28 ^ ((scrolled_row & 7) << 2)); + if(storage->vertical_scroll_lock_) { + static_row_info.pattern_address_base = bits<11>(AddressT(pattern_name_address & ((y & ~7) << 3))) - pattern_name_offset; + static_row_info.sub_row[0] = AddressT((y & 7) << 2); + static_row_info.sub_row[1] = 28 ^ AddressT((y & 7) << 2); + } else static_row_info = scrolled_row_info; + } + + void fetch_sprite(int sprite) { + auto &sprite_buffer = *base->fetch_sprite_buffer_; + sprite_buffer.active_sprites[sprite].x = + base->ram_[ + storage->sprite_attribute_table_address_ & bits<7>((sprite_buffer.active_sprites[sprite].index << 1) | 0) + ] - (storage->shift_sprites_8px_left_ ? 8 : 0); + const uint8_t name = base->ram_[ + storage->sprite_attribute_table_address_ & bits<7>((sprite_buffer.active_sprites[sprite].index << 1) | 1) + ] & (base->sprites_16x16_ ? ~1 : ~0); + + const AddressT graphic_location = + storage->sprite_generator_table_address_ & + bits<13>(AddressT((name << 5) | (sprite_buffer.active_sprites[sprite].row << 2))); + sprite_buffer.active_sprites[sprite].image[0] = base->ram_[graphic_location]; + sprite_buffer.active_sprites[sprite].image[1] = base->ram_[graphic_location+1]; + sprite_buffer.active_sprites[sprite].image[2] = base->ram_[graphic_location+2]; + sprite_buffer.active_sprites[sprite].image[3] = base->ram_[graphic_location+3]; + } + + void fetch_tile_name(int column) { + const RowInfo &row_info = column < 24 ? scrolled_row_info : static_row_info; + const size_t scrolled_column = (column - horizontal_offset) & 0x1f; + const size_t address = row_info.pattern_address_base + (scrolled_column << 1); + auto &line_buffer = *base->fetch_line_buffer_; + + line_buffer.tiles.flags[column] = base->ram_[address+1]; + base->tile_offset_ = AddressT( + (((line_buffer.tiles.flags[column]&1) << 8) | base->ram_[address]) << 5 + ) + row_info.sub_row[(line_buffer.tiles.flags[column]&4) >> 2]; + } + + void fetch_tile_pattern(int column) { + auto &line_buffer = *base->fetch_line_buffer_; + line_buffer.tiles.patterns[column][0] = base->ram_[base->tile_offset_]; + line_buffer.tiles.patterns[column][1] = base->ram_[base->tile_offset_+1]; + line_buffer.tiles.patterns[column][2] = base->ram_[base->tile_offset_+2]; + line_buffer.tiles.patterns[column][3] = base->ram_[base->tile_offset_+3]; + } + + void posit_sprite(int sprite) { + base->posit_sprite(sprite, base->ram_[storage->sprite_attribute_table_address_ & bits<8>(AddressT(sprite))], y); + } + + Base *const base; + const Storage *const storage; + const uint8_t y; + const int horizontal_offset; + RowInfo scrolled_row_info, static_row_info; +}; + +// MARK: - TMS Sequencers. + +template +struct RefreshSequencer { + RefreshSequencer(Base *base) : base(base) {} + + template void fetch() { + if(cycle < 26 || (cycle & 1) || cycle >= 154) { + base->do_external_slot(to_internal(cycle)); + } + } + + Base *const base; +}; + +template +struct TextSequencer { + template TextSequencer(Args&&... args) : fetcher(std::forward(args)...) {} + + template void fetch() { + // The first 30 and the final 4 slots are external. + if constexpr (cycle < 30 || cycle >= 150) { + fetcher.base->do_external_slot(to_internal(cycle)); + return; + } else { + // For the 120 slots in between follow a three-step pattern of: + constexpr int offset = cycle - 30; + constexpr auto column = AddressT(offset / 3); + switch(offset % 3) { + case 0: // (1) fetch tile name. + fetcher.fetch_name(column); + break; + case 1: // (2) external slot. + fetcher.base->do_external_slot(to_internal(cycle)); + break; + case 2: // (3) fetch tile pattern. + fetcher.fetch_pattern(column); + break; + } + } + } + + using AddressT = typename Base::AddressT; + TextFetcher fetcher; +}; + +template +struct CharacterSequencer { + template CharacterSequencer(Args&&... args) : + character_fetcher(std::forward(args)...), + sprite_fetcher(std::forward(args)...) {} + + template void fetch() { + if(cycle < 5) { + character_fetcher.base->do_external_slot(to_internal(cycle)); + } + + if(cycle == 5) { + // Fetch: n1, c2, pat2a, pat2b, y3, x3, n3, c3, pat3a, pat3b. + sprite_fetcher.fetch_pattern(2); + sprite_fetcher.fetch_location(3); + sprite_fetcher.fetch_pattern(3); + } + + if(cycle > 14 && cycle < 19) { + character_fetcher.base->do_external_slot(to_internal(cycle)); + } + + // Fetch 8 new sprite Y coordinates, to begin selecting sprites for next line. + if(cycle == 19) { + sprite_fetcher.fetch_y(0); sprite_fetcher.fetch_y(1); sprite_fetcher.fetch_y(2); sprite_fetcher.fetch_y(3); + sprite_fetcher.fetch_y(4); sprite_fetcher.fetch_y(5); sprite_fetcher.fetch_y(6); sprite_fetcher.fetch_y(7); + } + + // Body of line: tiles themselves, plus some additional potential sprites. + if(cycle >= 27 && cycle < 155) { + constexpr int offset = cycle - 27; + constexpr int block = offset >> 2; + constexpr int sub_block = offset & 3; + switch(sub_block) { + case 0: character_fetcher.fetch_name(block); break; + case 1: + if(!(block & 3)) { + character_fetcher.base->do_external_slot(to_internal(cycle)); + } else { + constexpr int sprite = 8 + ((block >> 2) * 3) + ((block & 3) - 1); + sprite_fetcher.fetch_y(sprite); + } + break; + case 2: + character_fetcher.fetch_pattern(block); + character_fetcher.fetch_colour(block); + break; + default: break; + } + } + + if(cycle >= 155 && cycle < 157) { + character_fetcher.base->do_external_slot(to_internal(cycle)); + } + + if(cycle == 157) { + // Fetch: y0, x0, n0, c0, pat0a, pat0b, y1, x1, n1, c1, pat1a, pat1b, y2, x2. + sprite_fetcher.fetch_location(0); + sprite_fetcher.fetch_pattern(0); + sprite_fetcher.fetch_location(1); + sprite_fetcher.fetch_pattern(1); + sprite_fetcher.fetch_location(2); + } + } + + using AddressT = typename Base::AddressT; + CharacterFetcher character_fetcher; + SpriteFetcher sprite_fetcher; +}; + +// MARK: - TMS fetch routines. + +template +template void Base::fetch_tms_refresh(uint8_t, int start, int end) { + RefreshSequencer sequencer(this); + dispatch(sequencer, start, end); +} + +template +template void Base::fetch_tms_text(uint8_t y, int start, int end) { + TextSequencer sequencer(this, y); + dispatch(sequencer, start, end); +} + +template +template void Base::fetch_tms_character(uint8_t y, int start, int end) { + CharacterSequencer sequencer(this, y); + dispatch(sequencer, start, end); +} + +// MARK: - Master System + +template +struct SMSSequencer { + template SMSSequencer(Args&&... args) : fetcher(std::forward(args)...) {} + + // Cf. https://www.smspower.org/forums/16485-GenesisMode4VRAMTiming with this implementation pegging + // window 0 to HSYNC low. + template void fetch() { + if(cycle < 3) { + fetcher.base->do_external_slot(to_internal(cycle)); + } + + if(cycle == 3) { + fetcher.fetch_sprite(4); + fetcher.fetch_sprite(5); + fetcher.fetch_sprite(6); + fetcher.fetch_sprite(7); + } + + if(cycle == 15 || cycle == 16) { + fetcher.base->do_external_slot(to_internal(cycle)); + } + + if(cycle == 17) { + fetcher.posit_sprite(0); fetcher.posit_sprite(1); fetcher.posit_sprite(2); fetcher.posit_sprite(3); + fetcher.posit_sprite(4); fetcher.posit_sprite(5); fetcher.posit_sprite(6); fetcher.posit_sprite(7); + fetcher.posit_sprite(8); fetcher.posit_sprite(9); fetcher.posit_sprite(10); fetcher.posit_sprite(11); + fetcher.posit_sprite(12); fetcher.posit_sprite(13); fetcher.posit_sprite(14); fetcher.posit_sprite(15); + } + + if(cycle >= 25 && cycle < 153) { + constexpr int offset = cycle - 25; + constexpr int block = offset >> 2; + constexpr int sub_block = offset & 3; + + switch(sub_block) { + default: break; + + case 0: fetcher.fetch_tile_name(block); break; + case 1: + if(!(block & 3)) { + fetcher.base->do_external_slot(to_internal(cycle)); + } else { + constexpr int sprite = (8 + ((block >> 2) * 3) + ((block & 3) - 1)) << 1; + fetcher.posit_sprite(sprite); + fetcher.posit_sprite(sprite+1); + } + break; + case 2: fetcher.fetch_tile_pattern(block); break; + } + } + + if(cycle >= 153 && cycle < 157) { + fetcher.base->do_external_slot(to_internal(cycle)); + } + + if(cycle == 157) { + fetcher.fetch_sprite(0); + fetcher.fetch_sprite(1); + fetcher.fetch_sprite(2); + fetcher.fetch_sprite(3); + } + + if(cycle >= 169) { + fetcher.base->do_external_slot(to_internal(cycle)); + } + } + + using AddressT = typename Base::AddressT; + SMSFetcher fetcher; +}; + +template +template void Base::fetch_sms([[maybe_unused]] uint8_t y, [[maybe_unused]] int start, [[maybe_unused]] int end) { + if constexpr (is_sega_vdp(personality)) { + SMSSequencer sequencer(this, y); + dispatch(sequencer, start, end); + } +} + +// MARK: - Yamaha + +template +template void Base::fetch_yamaha(uint8_t y, int end) { + CharacterFetcher character_fetcher(this, y); + TextFetcher text_fetcher(this, y); + SpriteFetcher sprite_fetcher(this, y); + + using Type = typename Storage::Event::Type; + while(Storage::next_event_->offset < end) { + switch(Storage::next_event_->type) { + case Type::External: + do_external_slot(Storage::next_event_->offset); + break; + + case Type::Name: + switch(mode) { + case ScreenMode::Text: { + const auto column = AddressT(Storage::next_event_->id << 1); + + text_fetcher.fetch_name(column, 0); + text_fetcher.fetch_name(column + 1, 1); + } break; + + case ScreenMode::YamahaText80: { + const auto column = AddressT(Storage::next_event_->id << 2); + const auto start = pattern_name_address_ & bits<12>(AddressT((y >> 3) * 80)); + + name_[0] = ram_[start + column + 0]; + name_[1] = ram_[start + column + 1]; + name_[2] = ram_[start + column + 2]; + name_[3] = ram_[start + column + 3]; + } break; + + case ScreenMode::Graphics: + case ScreenMode::MultiColour: + case ScreenMode::ColouredText: + character_fetcher.fetch_name(Storage::next_event_->id); + break; + + default: break; + } + break; + + case Type::Colour: + switch(mode) { + case ScreenMode::YamahaText80: { + const auto column = AddressT(Storage::next_event_->id); + const auto address = colour_table_address_ & bits<9>(AddressT((y >> 3) * 10)); + auto &line_buffer = *fetch_line_buffer_; + line_buffer.characters.flags[column] = ram_[address + column]; + } break; + + case ScreenMode::Graphics: + case ScreenMode::MultiColour: + case ScreenMode::ColouredText: + character_fetcher.fetch_colour(Storage::next_event_->id); + break; + + default: break; + } + break; + + case Type::Pattern: + switch(mode) { + case ScreenMode::Text: { + const auto column = AddressT(Storage::next_event_->id << 1); + + text_fetcher.fetch_pattern(column, 0); + text_fetcher.fetch_pattern(column + 1, 1); + } break; + + case ScreenMode::YamahaText80: { + const auto column = Storage::next_event_->id << 2; + const auto start = pattern_generator_table_address_ & bits<11>(AddressT(y & 7)); + auto &line_buffer = *fetch_line_buffer_; + + line_buffer.characters.shapes[column + 0] = ram_[start + AddressT(name_[0] << 3)]; + line_buffer.characters.shapes[column + 1] = ram_[start + AddressT(name_[1] << 3)]; + line_buffer.characters.shapes[column + 2] = ram_[start + AddressT(name_[2] << 3)]; + line_buffer.characters.shapes[column + 3] = ram_[start + AddressT(name_[3] << 3)]; + } break; + + case ScreenMode::Graphics: + case ScreenMode::MultiColour: + case ScreenMode::ColouredText: + character_fetcher.fetch_pattern(Storage::next_event_->id); + break; + + case ScreenMode::YamahaGraphics3: + // As per comment elsewhere; my _guess_ is that G3 is slotted as if it were + // a bitmap mode, with the three bytes that describe each column fitting into + // the relevant windows. + character_fetcher.fetch_name(Storage::next_event_->id); + character_fetcher.fetch_colour(Storage::next_event_->id); + character_fetcher.fetch_pattern(Storage::next_event_->id); + break; + + case ScreenMode::YamahaGraphics4: + case ScreenMode::YamahaGraphics5: { + const int column = Storage::next_event_->id << 2; + const auto start = bits<15>((y << 7) | column); + auto &line_buffer = *fetch_line_buffer_; + + line_buffer.bitmap[column + 0] = ram_[pattern_name_address_ & AddressT(start + 0)]; + line_buffer.bitmap[column + 1] = ram_[pattern_name_address_ & AddressT(start + 1)]; + line_buffer.bitmap[column + 2] = ram_[pattern_name_address_ & AddressT(start + 2)]; + line_buffer.bitmap[column + 3] = ram_[pattern_name_address_ & AddressT(start + 3)]; + } break; + + case ScreenMode::YamahaGraphics6: + case ScreenMode::YamahaGraphics7: { + const uint8_t *const ram2 = &ram_[65536]; + const int column = Storage::next_event_->id << 3; + const auto start = bits<15>((y << 7) | (column >> 1)); + auto &line_buffer = *fetch_line_buffer_; + + // Fetch from alternate banks. + line_buffer.bitmap[column + 0] = ram_[pattern_name_address_ & AddressT(start + 0) & 0xffff]; + line_buffer.bitmap[column + 1] = ram2[pattern_name_address_ & AddressT(start + 0) & 0xffff]; + line_buffer.bitmap[column + 2] = ram_[pattern_name_address_ & AddressT(start + 1) & 0xffff]; + line_buffer.bitmap[column + 3] = ram2[pattern_name_address_ & AddressT(start + 1) & 0xffff]; + line_buffer.bitmap[column + 4] = ram_[pattern_name_address_ & AddressT(start + 2) & 0xffff]; + line_buffer.bitmap[column + 5] = ram2[pattern_name_address_ & AddressT(start + 2) & 0xffff]; + line_buffer.bitmap[column + 6] = ram_[pattern_name_address_ & AddressT(start + 3) & 0xffff]; + line_buffer.bitmap[column + 7] = ram2[pattern_name_address_ & AddressT(start + 3) & 0xffff]; + } break; + + default: break; + } + break; + + case Type::SpriteY: + switch(mode) { + case ScreenMode::Blank: + case ScreenMode::Text: + case ScreenMode::YamahaText80: + // Ensure the compiler can discard character_fetcher in these modes. + break; + + default: + sprite_fetcher.fetch_y(Storage::next_event_->id); + break; + } + break; + + case Type::SpriteLocation: + switch(mode) { + case ScreenMode::Blank: + case ScreenMode::Text: + case ScreenMode::YamahaText80: + // Ensure the compiler can discard character_fetcher in these modes. + break; + + default: + sprite_fetcher.fetch_location(Storage::next_event_->id); + break; + } + break; + + case Type::SpritePattern: + switch(mode) { + case ScreenMode::Blank: + case ScreenMode::Text: + case ScreenMode::YamahaText80: + // Ensure the compiler can discard character_fetcher in these modes. + break; + + default: + sprite_fetcher.fetch_pattern(Storage::next_event_->id); + break; + } + break; + + default: break; + } + + ++Storage::next_event_; + } +} + + +template +template void Base::fetch_yamaha(uint8_t y, int, int end) { + if constexpr (is_yamaha_vdp(personality)) { + // Dispatch according to [supported] screen mode. +#define Dispatch(mode) case mode: fetch_yamaha(y, end); break; + switch(fetch_line_buffer_->screen_mode) { + default: break; + Dispatch(ScreenMode::Blank); + Dispatch(ScreenMode::Text); + Dispatch(ScreenMode::MultiColour); + Dispatch(ScreenMode::ColouredText); + Dispatch(ScreenMode::Graphics); + Dispatch(ScreenMode::YamahaText80); + Dispatch(ScreenMode::YamahaGraphics3); + Dispatch(ScreenMode::YamahaGraphics4); + Dispatch(ScreenMode::YamahaGraphics5); + Dispatch(ScreenMode::YamahaGraphics6); + Dispatch(ScreenMode::YamahaGraphics7); + } +#undef Dispatch + } +} + +// MARK: - Mega Drive + +// TODO. + +} diff --git a/Components/9918/Implementation/LineBuffer.hpp b/Components/9918/Implementation/LineBuffer.hpp new file mode 100644 index 000000000..4a2b7aca1 --- /dev/null +++ b/Components/9918/Implementation/LineBuffer.hpp @@ -0,0 +1,129 @@ +// +// LineBuffer.hpp +// Clock Signal +// +// Created by Thomas Harte on 12/02/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "AccessEnums.hpp" + +namespace TI::TMS { + +// Temporary buffers collect a representation of each line prior to pixel serialisation. + +struct SpriteBuffer { + // An active sprite is one that has been selected for composition onto + // _this_ line. + struct ActiveSprite { + int index = 0; // The original in-table index of this sprite. + int row = 0; // The row of the sprite that should be drawn. + int x = 0; // The sprite's x position on screen. + + uint8_t image[4]; // Up to four bytes of image information. + // + // In practice: + // + // Master System mode: the four bytes of this 8x8 sprite; + // TMS and Yamaha: [0] = the left half of this sprite; [1] = the right side (if 16x16 sprites enabled); [2] = colour, early-clock bit, etc. + int shift_position = 0; // An offset representing how much of the image information has already been drawn. + + // Yamaha helpers. + bool opaque() const { + return !(image[2] & 0x40); + } + + /// @returns @c 0x20 if this sprite should generate collisions; @c 0x00 otherwise. + int collision_bit() const { + return ((image[2] & 0x20) | ((image[2] & 0x40) >> 1)) ^ 0x20; + } + + // Yamaha and TMS helpers. + int early_clock() const { + return (image[2] & 0x80) >> 2; + } + } active_sprites[8]; + + int active_sprite_slot = 0; // A pointer to the slot into which a new active sprite will be deposited, if required. + bool sprites_stopped = false; // A special TMS feature is that a sentinel value can be used to prevent any further sprites + // being evaluated for display. This flag determines whether the sentinel has yet been reached. + uint8_t sprite_terminator = 0; + +#ifndef NDEBUG + static constexpr bool test_is_filling = true; +#else + static constexpr bool test_is_filling = false; +#endif + bool is_filling = false; + + void reset_sprite_collection(); +}; + +struct LineBuffer { + LineBuffer() {} + + // The fetch mode describes the proper timing diagram for this line; + // screen mode captures proper output mode. + FetchMode fetch_mode = FetchMode::Text; + ScreenMode screen_mode = ScreenMode::Text; + VerticalState vertical_state = VerticalState::Blank; + SpriteBuffer *sprites = nullptr; + + // Holds the horizontal scroll position to apply to this line; + // of those VDPs currently implemented, affects the Master System only. + uint8_t latched_horizontal_scroll = 0; + + // The names array holds pattern names, as an offset into memory, and + // potentially flags also. + union { + // This struct captures maximal potential detail across the TMS9918 + // and Sega VDP for tiled modes (plus multicolour). + struct { + uint8_t flags[32]{}; + + // The patterns array holds tile patterns, corresponding 1:1 with names. + // Four bytes per pattern is the maximum required by any + // currently-implemented VDP. + uint8_t patterns[32][4]{}; + } tiles; + + // The Yamaha and TMS both have text modes, with the former going up to + // 80 columns plus 10 bytes of colour-esque flags. + struct { + uint8_t shapes[80]; + uint8_t flags[10]; + } characters; + + // The Yamaha VDP also has a variety of bitmap modes, + // the widest of which is 512px @ 4bpp. + uint8_t bitmap[256]; + }; + + /* + Horizontal layout (on a 342-cycle clock): + + 15 cycles right border + 58 cycles blanking & sync + 13 cycles left border + + ... i.e. to cycle 86, then: + + border up to first_pixel_output_column; + pixels up to next_border_column; + border up to the end. + + e.g. standard 256-pixel modes will want to set + first_pixel_output_column = 86, next_border_column = 342. + */ + int first_pixel_output_column = 94; + int next_border_column = 334; + int pixel_count = 256; +}; + +struct LineBufferPointer { + int row = 0, column = 0; +}; + +} diff --git a/Components/9918/Implementation/LineLayout.hpp b/Components/9918/Implementation/LineLayout.hpp new file mode 100644 index 000000000..d170f5c4b --- /dev/null +++ b/Components/9918/Implementation/LineLayout.hpp @@ -0,0 +1,77 @@ +// +// LineLayout.hpp +// Clock Signal +// +// Created by Thomas Harte on 18/05/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +namespace TI::TMS { + +template struct LineLayout; + +// Line layout is: +// +// [0, EndOfSync] sync +// (EndOfSync, StartOfColourBurst] blank +// (StartOfColourBurst, EndOfColourBurst] colour burst +// (EndOfColourBurst, EndOfLeftErase] blank +// (EndOfLeftErase, EndOfLeftBorder] border colour +// (EndOfLeftBorder, EndOfPixels] pixel content +// (EndOfPixels, EndOfRightBorder] border colour +// [EndOfRightBorder, ] blank +// +// ... with minor caveats: +// * horizontal adjust on the Yamaha VDPs is applied to EndOfLeftBorder and EndOfPixels; +// * the Sega VDPs may programatically extend the left border; and +// * text mode on all VDPs adjusts border width. + +template struct LineLayout> { + constexpr static int StartOfSync = 0; + constexpr static int EndOfSync = 26; + constexpr static int StartOfColourBurst = 29; + constexpr static int EndOfColourBurst = 43; + constexpr static int EndOfLeftErase = 50; + constexpr static int EndOfLeftBorder = 63; + constexpr static int EndOfPixels = 319; + constexpr static int EndOfRightBorder = 334; + + constexpr static int CyclesPerLine = 342; + + constexpr static int TextModeEndOfLeftBorder = 69; + constexpr static int TextModeEndOfPixels = 309; + + constexpr static int ModeLatchCycle = 36; // Just a guess; correlates with the known 144 for the Yamaha VDPs, + // and falls into the collection gap between the final sprite + // graphics and the initial tiles or pixels. + + /// The number of internal cycles that must elapse between a request to read or write and + /// it becoming a candidate for action. + constexpr static int VRAMAccessDelay = 6; +}; + +template struct LineLayout> { + constexpr static int StartOfSync = 0; + constexpr static int EndOfSync = 100; + constexpr static int StartOfColourBurst = 113; + constexpr static int EndOfColourBurst = 167; + constexpr static int EndOfLeftErase = 202; + constexpr static int EndOfLeftBorder = 258; + constexpr static int EndOfPixels = 1282; + constexpr static int EndOfRightBorder = 1341; + + constexpr static int CyclesPerLine = 1368; + + constexpr static int TextModeEndOfLeftBorder = 294; + constexpr static int TextModeEndOfPixels = 1254; + + constexpr static int ModeLatchCycle = 144; + + /// The number of internal cycles that must elapse between a request to read or write and + /// it becoming a candidate for action. + constexpr static int VRAMAccessDelay = 16; +}; + +} diff --git a/Components/9918/Implementation/PersonalityTraits.hpp b/Components/9918/Implementation/PersonalityTraits.hpp new file mode 100644 index 000000000..adc536192 --- /dev/null +++ b/Components/9918/Implementation/PersonalityTraits.hpp @@ -0,0 +1,47 @@ +// +// PersonalityTraits.hpp +// Clock Signal +// +// Created by Thomas Harte on 06/01/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +namespace TI::TMS { + +// Genus determinants for the various personalityes. +constexpr bool is_sega_vdp(Personality p) { + return p >= Personality::SMSVDP; +} + +constexpr bool is_yamaha_vdp(Personality p) { + return p == Personality::V9938 || p == Personality::V9958; +} + +// i.e. one with the original internal timings. +constexpr bool is_classic_vdp(Personality p) { + return + p == Personality::TMS9918A || + p == Personality::SMSVDP || + p == Personality::SMS2VDP || + p == Personality::GGVDP; +} + +constexpr size_t memory_size(Personality p) { + switch(p) { + case TI::TMS::TMS9918A: + case TI::TMS::SMSVDP: + case TI::TMS::SMS2VDP: + case TI::TMS::GGVDP: return 16 * 1024; + case TI::TMS::MDVDP: return 64 * 1024; + case TI::TMS::V9938: return 128 * 1024; + case TI::TMS::V9958: return 192 * 1024; + } +} + +constexpr size_t memory_mask(Personality p) { + return memory_size(p) - 1; +} + +} diff --git a/Components/9918/Implementation/Storage.hpp b/Components/9918/Implementation/Storage.hpp new file mode 100644 index 000000000..1d3389781 --- /dev/null +++ b/Components/9918/Implementation/Storage.hpp @@ -0,0 +1,487 @@ +// +// +// Storage.hpp +// Clock Signal +// +// Created by Thomas Harte on 12/02/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "LineBuffer.hpp" +#include "YamahaCommands.hpp" + +#include +#include + +namespace TI::TMS { + +/// A container for personality-specific storage; see specific instances below. +template struct Storage { +}; + +template <> struct Storage { + using AddressT = uint16_t; + + void begin_line(ScreenMode, bool) {} +}; + +struct YamahaFetcher { + public: + /// Describes an _observable_ memory access event. i.e. anything that it is safe + /// (and convenient) to treat as atomic in between external slots. + struct Event { + /// Offset of the _beginning_ of the event. Not completely arbitrarily: this is when + /// external data must be ready by in order to take part in those slots. + uint16_t offset = 1368; + enum class Type: uint8_t { + /// A slot for reading or writing data on behalf of the CPU or the command engine. + External, + + // + // Sprites. + // + SpriteY, + SpriteLocation, + SpritePattern, + + // + // Backgrounds. + // + Name, + Colour, + Pattern, + } type = Type::External; + uint8_t id = 0; + + constexpr Event(Type type, uint8_t id = 0) noexcept : + type(type), + id(id) {} + + constexpr Event() noexcept {} + }; + + // State that tracks fetching position within a line. + const Event *next_event_ = nullptr; + + // Sprite collection state. + bool sprites_enabled_ = true; + + protected: + /// @return 1 + the number of times within a line that @c GeneratorT produces an event. + template static constexpr size_t events_size() { + size_t size = 0; + for(int c = 0; c < 1368; c++) { + const auto event_type = GeneratorT::event(c); + size += event_type.has_value(); + } + return size + 1; + } + + /// @return An array of all events generated by @c GeneratorT in line order. + template ()> + static constexpr std::array events() { + std::array result{}; + size_t index = 0; + for(int c = 0; c < 1368; c++) { + // Specific personality doesn't matter here; both Yamahas use the same internal timing. + const auto event = GeneratorT::event(from_internal(c)); + if(!event) { + continue; + } + result[index] = *event; + result[index].offset = uint16_t(c); + ++index; + } + result[index] = Event(); + return result; + } + + struct StandardGenerators { + static constexpr std::optional external_every_eight(int index) { + if(index & 7) return std::nullopt; + return Event::Type::External; + } + }; + + struct RefreshGenerator { + static constexpr std::optional event(int grauw_index) { + // From 0 to 126: CPU/CMD slots at every cycle divisible by 8. + if(grauw_index < 126) { + return StandardGenerators::external_every_eight(grauw_index - 0); + } + + // From 164 to 1234: eight-cycle windows, the first 15 of each 16 being + // CPU/CMD and the final being refresh. + if(grauw_index >= 164 && grauw_index < 1234) { + const int offset = grauw_index - 164; + if(offset & 7) return std::nullopt; + if(((offset >> 3) & 15) == 15) return std::nullopt; + return Event::Type::External; + } + + // From 1268 to 1330: CPU/CMD slots at every cycle divisible by 8. + if(grauw_index >= 1268 && grauw_index < 1330) { + return StandardGenerators::external_every_eight(grauw_index - 1268); + } + + // A CPU/CMD at 1334. + if(grauw_index == 1334) { + return Event::Type::External; + } + + // From 1344 to 1366: CPU/CMD slots every cycle divisible by 8. + if(grauw_index >= 1344 && grauw_index < 1366) { + return StandardGenerators::external_every_eight(grauw_index - 1344); + } + + // Otherwise: nothing. + return std::nullopt; + } + }; + + template struct BitmapGenerator { + static constexpr std::optional event(int grauw_index) { + if(!include_sprites) { + // Various standard zones of one-every-eight external slots. + if(grauw_index < 124) { + return StandardGenerators::external_every_eight(grauw_index + 2); + } + if(grauw_index > 1266) { + return StandardGenerators::external_every_eight(grauw_index - 1266); + } + } else { + // This records collection points for all data for selected sprites. + // There's only four of them (each site covering two sprites), + // so it's clearer just to be explicit. + // + // There's also a corresponding number of extra external slots to spell out. + switch(grauw_index) { + default: break; + case 1238: return Event(Event::Type::SpriteLocation, 0); + case 1302: return Event(Event::Type::SpriteLocation, 2); + case 2: return Event(Event::Type::SpriteLocation, 4); + case 66: return Event(Event::Type::SpriteLocation, 6); + case 1270: return Event(Event::Type::SpritePattern, 0); + case 1338: return Event(Event::Type::SpritePattern, 2); + case 34: return Event(Event::Type::SpritePattern, 4); + case 98: return Event(Event::Type::SpritePattern, 6); + case 1264: case 1330: case 28: case 92: + return Event::Type::External; + } + } + + if(grauw_index >= 162 && grauw_index < 176) { + return StandardGenerators::external_every_eight(grauw_index - 162); + } + + // Everywhere else the pattern is: + // + // external or sprite y, external, data block + // + // Subject to caveats: + // + // 1) the first data block is just a dummy fetch with no side effects, + // so this emulator declines to record it; and + // 2) every fourth block, the second external is actually a refresh. + // + if(grauw_index >= 182 && grauw_index < 1238) { + const int offset = grauw_index - 182; + const int block = offset / 32; + const int sub_block = offset & 31; + + switch(sub_block) { + default: return std::nullopt; + case 0: + if(include_sprites) { + // Don't include the sprite post-amble (i.e. a spurious read with no side effects). + if(block < 32) { + return Event(Event::Type::SpriteY, uint8_t(block)); + } + } else { + return Event::Type::External; + } + case 6: + if((block & 3) != 3) { + return Event::Type::External; + } + break; + case 12: + if(block) { + return Event(Event::Type::Pattern, uint8_t(block - 1)); + } + break; + } + } + + return std::nullopt; + } + }; + + struct TextGenerator { + static constexpr std::optional event(int grauw_index) { + // Capture various one-in-eight zones. + if(grauw_index < 72) { + return StandardGenerators::external_every_eight(grauw_index - 2); + } + if(grauw_index >= 166 && grauw_index < 228) { + return StandardGenerators::external_every_eight(grauw_index - 166); + } + if(grauw_index >= 1206 && grauw_index < 1332) { + return StandardGenerators::external_every_eight(grauw_index - 1206); + } + if(grauw_index == 1336) { + return Event::Type::External; + } + if(grauw_index >= 1346) { + return StandardGenerators::external_every_eight(grauw_index - 1346); + } + + // Elsewhere... + if(grauw_index >= 246) { + const int offset = grauw_index - 246; + const int block = offset / 48; + const int sub_block = offset % 48; + switch(sub_block) { + default: break; + case 0: return Event(Event::Type::Name, uint8_t(block)); + case 18: return (block & 1) ? Event::Type::External : Event(Event::Type::Colour, uint8_t(block >> 1)); + case 24: return Event(Event::Type::Pattern, uint8_t(block)); + } + } + + return std::nullopt; + } + }; + + struct CharacterGenerator { + static constexpr std::optional event(int grauw_index) { + // Grab sprite events. + switch(grauw_index) { + default: break; + case 1242: return Event(Event::Type::SpriteLocation, 0); + case 1306: return Event(Event::Type::SpriteLocation, 1); + case 6: return Event(Event::Type::SpriteLocation, 2); + case 70: return Event(Event::Type::SpriteLocation, 3); + case 1274: return Event(Event::Type::SpritePattern, 0); + case 1342: return Event(Event::Type::SpritePattern, 1); + case 38: return Event(Event::Type::SpritePattern, 2); + case 102: return Event(Event::Type::SpritePattern, 3); + case 1268: case 1334: case 32: case 96: return Event::Type::External; + } + + if(grauw_index >= 166 && grauw_index < 180) { + return StandardGenerators::external_every_eight(grauw_index - 166); + } + + if(grauw_index >= 182 && grauw_index < 1238) { + const int offset = grauw_index - 182; + const int block = offset / 32; + const int sub_block = offset & 31; + switch(sub_block) { + case 0: if(block > 0) return Event(Event::Type::Name, uint8_t(block - 1)); + case 6: if((sub_block & 3) != 3) return Event::Type::External; + case 12: if(block < 32) return Event(Event::Type::SpriteY, uint8_t(block)); + case 18: if(block > 0) return Event(Event::Type::Pattern, uint8_t(block - 1)); + case 24: if(block > 0) return Event(Event::Type::Colour, uint8_t(block - 1)); + } + } + + return std::nullopt; + } + }; +}; + +struct YamahaCommandState { + CommandContext command_context_; + ModeDescription mode_description_; + std::unique_ptr command_ = nullptr; + + enum class CommandStep { + None, + + CopySourcePixelToStatus, + + ReadSourcePixel, + ReadDestinationPixel, + WritePixel, + + ReadSourceByte, + WriteByte, + }; + CommandStep next_command_step_ = CommandStep::None; + int minimum_command_column_ = 0; + uint8_t command_latch_ = 0; + + void update_command_step(int current_column) { + if(!command_) { + next_command_step_ = CommandStep::None; + return; + } + if(command_->done()) { + command_ = nullptr; + next_command_step_ = CommandStep::None; + return; + } + + minimum_command_column_ = current_column + command_->cycles; + switch(command_->access) { + case Command::AccessType::ReadPoint: + next_command_step_ = CommandStep::CopySourcePixelToStatus; + break; + + case Command::AccessType::CopyPoint: + next_command_step_ = CommandStep::ReadSourcePixel; + break; + case Command::AccessType::PlotPoint: + next_command_step_ = CommandStep::ReadDestinationPixel; + break; + + case Command::AccessType::WaitForColourReceipt: + // i.e. nothing to do until a colour is received. + next_command_step_ = CommandStep::None; + break; + + case Command::AccessType::CopyByte: + next_command_step_ = CommandStep::ReadSourceByte; + break; + case Command::AccessType::WriteByte: + next_command_step_ = CommandStep::WriteByte; + break; + } + } +}; + +// Yamaha-specific storage. +template struct Storage>: public YamahaFetcher, public YamahaCommandState { + using AddressT = uint32_t; + + // The Yamaha's (optional in real hardware) additional 64kb of expansion RAM. + // This is a valid target and source for the command engine, but can't be used as a source for current video data. + std::array expansion_ram_; + + // Register indirections. + int selected_status_ = 0; + int indirect_register_ = 0; + bool increment_indirect_register_ = false; + + // Output horizontal and vertical adjustment, plus the selected vertical offset (i.e. hardware scroll). + int adjustment_[2]{}; + uint8_t vertical_offset_ = 0; + + // The palette, plus a shadow copy in which colour 0 is not the current palette colour 0, + // but is rather the current global background colour. This simplifies flow when colour 0 + // is set as transparent. + std::array palette_{}; + std::array background_palette_{}; + bool solid_background_ = true; + + // Transient state for palette setting. + uint8_t new_colour_ = 0; + uint8_t palette_entry_ = 0; + bool palette_write_phase_ = false; + + // Recepticle for all five bits of the current screen mode. + uint8_t mode_ = 0; + + // Used ephemerally during drawing to compound sprites with the 'CC' + // (compound colour?) bit set. + uint8_t sprite_cache_[8][32]{}; + + // Text blink colours. + uint8_t blink_text_colour_ = 0; + uint8_t blink_background_colour_ = 0; + + // Blink state (which is also affects even/odd page display in applicable modes). + int in_blink_ = 1; + uint8_t blink_periods_ = 0; + uint8_t blink_counter_ = 0; + + // Additional things exposed by status registers. + uint8_t colour_status_ = 0; + uint16_t colour_location_ = 0; + uint16_t collision_location_[2]{}; + bool line_matches_ = false; + + Storage() noexcept { + // Seed to something valid. + next_event_ = refresh_events.data(); + } + + /// Resets line-ephemeral state for a new line. + void begin_line(ScreenMode mode, bool is_refresh) { + if(is_refresh) { + next_event_ = refresh_events.data(); + return; + } + + switch(mode) { + case ScreenMode::YamahaText80: + case ScreenMode::Text: + next_event_ = text_events.data(); + break; + + case ScreenMode::MultiColour: + case ScreenMode::YamahaGraphics1: + case ScreenMode::YamahaGraphics2: + next_event_ = character_events.data(); + break; + + case ScreenMode::YamahaGraphics3: // TODO: verify; my guess is that G3 is timed like a bitmap mode + // in order to fit the pattern for sprite mode 2. Just a guess. + default: + next_event_ = sprites_enabled_ ? sprites_events.data() : no_sprites_events.data(); + break; + } + } + + private: + static constexpr auto refresh_events = events(); + static constexpr auto no_sprites_events = events>(); + static constexpr auto sprites_events = events>(); + static constexpr auto text_events = events(); + static constexpr auto character_events = events(); +}; + +// Master System-specific storage. +template struct Storage> { + using AddressT = uint16_t; + + // The SMS VDP has a programmer-set colour palette, with a dedicated patch of RAM. But the RAM is only exactly + // fast enough for the pixel clock. So when the programmer writes to it, that causes a one-pixel glitch; there + // isn't the bandwidth for the read both write to occur simultaneously. The following buffer therefore keeps + // track of pending collisions, for visual reproduction. + struct CRAMDot { + LineBufferPointer location; + uint32_t value; + }; + std::vector upcoming_cram_dots_; + + // The Master System's additional colour RAM. + uint32_t colour_ram_[32]; + bool cram_is_selected_ = false; + + // Programmer-set flags. + bool vertical_scroll_lock_ = false; + bool horizontal_scroll_lock_ = false; + bool hide_left_column_ = false; + bool shift_sprites_8px_left_ = false; + bool mode4_enable_ = false; + uint8_t horizontal_scroll_ = 0; + uint8_t vertical_scroll_ = 0; + + // Holds the vertical scroll position for this frame; this is latched + // once and cannot dynamically be changed until the next frame. + uint8_t latched_vertical_scroll_ = 0; + + // Various resource addresses with VDP-version-specific modifications + // built in. + AddressT pattern_name_address_; + AddressT sprite_attribute_table_address_; + AddressT sprite_generator_table_address_; + + void begin_line(ScreenMode, bool) {} +}; + +} diff --git a/Components/9918/Implementation/YamahaCommands.hpp b/Components/9918/Implementation/YamahaCommands.hpp new file mode 100644 index 000000000..30c4e1fdb --- /dev/null +++ b/Components/9918/Implementation/YamahaCommands.hpp @@ -0,0 +1,381 @@ +// +// YamahaCommands.hpp +// Clock Signal +// +// Created by Thomas Harte on 26/01/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "AccessEnums.hpp" + +namespace TI::TMS { + +// MARK: - Generics. + +struct Vector { + int v[2]{}; + + template void set(uint8_t value) { + constexpr uint8_t mask = high ? (offset ? 0x3 : 0x1) : 0xff; + constexpr int shift = high ? 8 : 0; + v[offset] = (v[offset] & ~(mask << shift)) | ((value & mask) << shift); + } + + template void add(int amount) { + v[offset] += amount; + + if constexpr (offset == 1) { + v[offset] &= 0x3ff; + } else { + v[offset] &= 0x1ff; + } + } + + Vector & operator += (const Vector &rhs) { + add<0>(rhs.v[0]); + add<1>(rhs.v[1]); + return *this; + } +}; + +struct Colour { + void set(uint8_t value) { + colour = value; + colour4bpp = uint8_t((value & 0xf) | (value << 4)); + colour2bpp = uint8_t((colour4bpp & 0x33) | ((colour4bpp & 0x33) << 2)); + } + + void reset() { + colour = 0x00; + colour4bpp = 0xff; + } + + bool has_value() const { + return (colour & 0xf) == (colour4bpp & 0xf); + } + + /// Colour as written by the CPU. + uint8_t colour = 0x00; + /// The low four bits of the CPU-written colour, repeated twice. + uint8_t colour4bpp = 0xff; + /// The low two bits of the CPU-written colour, repeated four times. + uint8_t colour2bpp = 0xff; +}; + +struct CommandContext { + Vector source; + Vector destination; + Vector size; + + uint8_t arguments = 0; + Colour colour; + Colour latched_colour; + + enum class LogicalOperation { + Copy = 0b0000, + And = 0b0001, + Or = 0b0010, + Xor = 0b0011, + Not = 0b0100, + }; + LogicalOperation pixel_operation; + bool test_source; +}; + +struct ModeDescription { + int width = 256; + int pixels_per_byte = 4; + bool rotate_address = false; + int start_cycle = 0; + int end_cycle = 0; +}; + +struct Command { + // In net: + // + // This command is blocked until @c access has been performed, reading + // from or writing to @c value. It should not be performed until at least + // @c cycles have passed. + enum class AccessType { + /// Plots a single pixel of the current contextual colour at @c destination, + /// which occurs as a read, then a 24-cycle gap, then a write. + PlotPoint, + + /// Blocks until the next CPU write to the colour register. + WaitForColourReceipt, + + /// Writes an entire byte to the address containing the current @c destination. + WriteByte, + + /// Copies a single pixel from @c source location to @c destination, + /// being a read, a 32-cycle gap, then a PlotPoint. + CopyPoint, + + /// Copies a complete byte from @c source location to @c destination, + /// being a read, a 24-cycle gap, then a write. + CopyByte, + + /// Copies a single pixel from @c source to the colour status register. + ReadPoint, + +// ReadByte, +// WaitForColourSend, + }; + AccessType access = AccessType::PlotPoint; + int cycles = 0; + bool is_cpu_transfer = false; + bool y_only = false; + + /// Current command parameters. + CommandContext &context; + ModeDescription &mode_description; + Command(CommandContext &context, ModeDescription &mode_description) : context(context), mode_description(mode_description) {} + virtual ~Command() {} + + /// @returns @c true if all output from this command is done; @c false otherwise. + virtual bool done() = 0; + + /// Repopulates the fields above with the next action to take, being provided with the + /// number of pixels per byte in the current screen mode. + virtual void advance() = 0; + + protected: + template void advance_axis(int offset = 1) { + context.destination.add(context.arguments & (0x4 << axis) ? -offset : offset); + if constexpr (include_source) { + context.source.add(context.arguments & (0x4 << axis) ? -offset : offset); + } + } +}; + +namespace Commands { + +// MARK: - Line drawing. + +/// Implements the LINE command, which is plain-old Bresenham. +/// +/// Per Grauw timing is: +/// +/// * 88 cycles between every pixel plot; +/// * plus an additional 32 cycles if a step along the minor axis is taken. +struct Line: public Command { + public: + Line(CommandContext &context, ModeDescription &mode_description) : Command(context, mode_description) { + // context.destination = start position; + // context.size.v[0] = long side dots; + // context.size.v[1] = short side dots; + // context.arguments => direction + + position_ = context.size.v[1]; + numerator_ = position_ << 1; + denominator_ = context.size.v[0] << 1; + + cycles = 32; + access = AccessType::PlotPoint; + } + + bool done() final { + return !context.size.v[0]; + } + + void advance() final { + --context.size.v[0]; + cycles = 88; + + // b0: 1 => long direction is y; + // 0 => long direction is x. + // + // b2: 1 => x direction is left; + // 0 => x direction is right. + // + // b3: 1 => y direction is up; + // 0 => y direction is down. + if(context.arguments & 0x1) { + advance_axis<1, false>(); + } else { + advance_axis<0, false>(); + } + + position_ -= numerator_; + if(position_ < 0) { + position_ += denominator_; + cycles += 32; + + if(context.arguments & 0x1) { + advance_axis<0, false>(); + } else { + advance_axis<1, false>(); + } + } + } + + private: + int position_, numerator_, denominator_; +}; + +// MARK: - Single pixel manipulation. + +/// Implements the PSET command, which plots a single pixel and POINT, which reads one. +/// +/// No timings are documented, so this'll output or input as quickly as possible. +template struct Point: public Command { + public: + Point(CommandContext &context, ModeDescription &mode_description) : Command(context, mode_description) { + cycles = 0; // TODO. + access = is_read ? AccessType::ReadPoint : AccessType::PlotPoint; + } + + bool done() final { + return done_; + } + + void advance() final { + done_ = true; + } + + private: + bool done_ = false; +}; + +// MARK: - Rectangular base. + +/// Useful base class for anything that does logical work in a rectangle. +template struct Rectangle: public Command { + public: + Rectangle(CommandContext &context, ModeDescription &mode_description) : Command(context, mode_description) { + if constexpr (include_source) { + start_x_[0] = context.source.v[0]; + } + start_x_[1] = context.destination.v[0]; + width_ = context.size.v[0]; + + if(!width_) { + // Width = 0 => maximal width for this mode. + // (aside: it's still unclear to me whether commands are + // automatically clipped to the display; I think so but + // don't want to spend any time on it until I'm certain) +// context.size.v[0] = width_ = mode_description.width; + } + } + + /// Advances the current destination and, if @c include_source is @c true also the source; + /// @returns @c true if a new row was started; @c false otherwise. + bool advance_pixel() { + if constexpr (logical) { + advance_axis<0, include_source>(); + --context.size.v[0]; + + if(context.size.v[0]) { + return false; + } + } else { + advance_axis<0, include_source>(mode_description.pixels_per_byte); + context.size.v[0] -= mode_description.pixels_per_byte; + + if(context.size.v[0] & ~(mode_description.pixels_per_byte - 1)) { + return false; + } + } + + context.size.v[0] = width_; + if constexpr (include_source) { + context.source.v[0] = start_x_[0]; + } + context.destination.v[0] = start_x_[1]; + + advance_axis<1, include_source>(); + --context.size.v[1]; + + return true; + } + + bool done() final { + return !context.size.v[1] || !width_; + } + + private: + int start_x_[2]{}, width_ = 0; +}; + +// MARK: - Rectangular moves to/from CPU. + +template struct MoveFromCPU: public Rectangle { + MoveFromCPU(CommandContext &context, ModeDescription &mode_description) : Rectangle(context, mode_description) { + Command::is_cpu_transfer = true; + + // This command is started with the first colour ready to transfer. + Command::cycles = 32; + Command::access = logical ? Command::AccessType::PlotPoint : Command::AccessType::WriteByte; + } + + void advance() final { + switch(Command::access) { + default: break; + + case Command::AccessType::WaitForColourReceipt: + Command::cycles = 32; + Command::access = logical ? Command::AccessType::PlotPoint : Command::AccessType::WriteByte; + break; + + case Command::AccessType::WriteByte: + case Command::AccessType::PlotPoint: + Command::cycles = 0; + Command::access = Command::AccessType::WaitForColourReceipt; + if(Rectangle::advance_pixel()) { + Command::cycles = 64; + // TODO: I'm not sure this will be honoured per the outer wrapping. + } + break; + } + } +}; + +// MARK: - Rectangular moves within VRAM. + +enum class MoveType { + Logical, + HighSpeed, + YOnly, +}; + +template struct Move: public Rectangle { + static constexpr bool is_logical = type == MoveType::Logical; + static constexpr bool is_y_only = type == MoveType::YOnly; + using RectangleBase = Rectangle; + + Move(CommandContext &context, ModeDescription &mode_description) : RectangleBase(context, mode_description) { + Command::access = is_logical ? Command::AccessType::CopyPoint : Command::AccessType::CopyByte; + Command::cycles = is_y_only ? 0 : 64; + Command::y_only = is_y_only; + } + + void advance() final { + Command::cycles = is_y_only ? 40 : 64; + if(RectangleBase::advance_pixel()) { + Command::cycles += is_y_only ? 0 : 64; + } + } +}; + +// MARK: - Rectangular fills. + +template struct Fill: public Rectangle { + using RectangleBase = Rectangle; + + Fill(CommandContext &context, ModeDescription &mode_description) : RectangleBase(context, mode_description) { + Command::cycles = logical ? 64 : 56; + Command::access = logical ? Command::AccessType::PlotPoint : Command::AccessType::WriteByte; + } + + void advance() final { + Command::cycles = logical ? 72 : 48; + if(RectangleBase::advance_pixel()) { + Command::cycles += logical ? 64 : 56; + } + } +}; + +} +} diff --git a/Components/AY38910/AY38910.hpp b/Components/AY38910/AY38910.hpp index b06a92b2b..eee96c093 100644 --- a/Components/AY38910/AY38910.hpp +++ b/Components/AY38910/AY38910.hpp @@ -6,16 +6,14 @@ // Copyright 2016 Thomas Harte. All rights reserved. // -#ifndef AY_3_8910_hpp -#define AY_3_8910_hpp +#pragma once #include "../../Outputs/Speaker/Implementation/SampleSource.hpp" #include "../../Concurrency/AsyncTaskQueue.hpp" #include "../../Reflection/Struct.hpp" -namespace GI { -namespace AY38910 { +namespace GI::AY38910 { /*! A port handler provides all input for an AY's two 8-bit ports, and may optionally receive @@ -220,6 +218,3 @@ struct State: public Reflection::StructImpl { }; } -} - -#endif /* AY_3_8910_hpp */ diff --git a/Components/AppleClock/AppleClock.hpp b/Components/AppleClock/AppleClock.hpp index 0b06bb2cf..5a45d4179 100644 --- a/Components/AppleClock/AppleClock.hpp +++ b/Components/AppleClock/AppleClock.hpp @@ -6,13 +6,11 @@ // Copyright © 2019 Thomas Harte. All rights reserved. // -#ifndef Apple_RealTimeClock_hpp -#define Apple_RealTimeClock_hpp +#pragma once #include -namespace Apple { -namespace Clock { +namespace Apple::Clock { /*! Models Apple's real-time clocks, as contained in the Macintosh and IIgs. @@ -196,7 +194,7 @@ class SerialClock: public ClockStorage { Sets the current clock and data inputs to the clock. */ void set_input(bool clock, bool data) { - // The data line is valid when the clock transitions to level 0. + // The data line is valid when the clock transitions to level 0. if(clock && !previous_clock_) { // Shift into the command_ register, no matter what. command_ = uint16_t((command_ << 1) | (data ? 1 : 0)); @@ -294,6 +292,3 @@ class ParallelClock: public ClockStorage { }; } -} - -#endif /* Apple_RealTimeClock_hpp */ diff --git a/Components/AudioToggle/AudioToggle.hpp b/Components/AudioToggle/AudioToggle.hpp index 4390361c6..8209b4653 100644 --- a/Components/AudioToggle/AudioToggle.hpp +++ b/Components/AudioToggle/AudioToggle.hpp @@ -6,8 +6,7 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef AudioToggle_hpp -#define AudioToggle_hpp +#pragma once #include "../../Outputs/Speaker/Implementation/SampleSource.hpp" #include "../../Concurrency/AsyncTaskQueue.hpp" @@ -38,5 +37,3 @@ class Toggle: public Outputs::Speaker::SampleSource { }; } - -#endif /* AudioToggle_hpp */ diff --git a/Components/DiskII/DiskII.cpp b/Components/DiskII/DiskII.cpp index 772cc2380..19d3febe7 100644 --- a/Components/DiskII/DiskII.cpp +++ b/Components/DiskII/DiskII.cpp @@ -13,7 +13,7 @@ using namespace Apple; -namespace { +namespace { const uint8_t input_command = 0x4; // i.e. Q6 const uint8_t input_mode = 0x8; // i.e. Q7 const uint8_t input_flux = 0x1; @@ -23,8 +23,14 @@ DiskII::DiskII(int clock_rate) : clock_rate_(clock_rate), inputs_(input_command), drives_{ - Storage::Disk::Drive{clock_rate, 300, 1}, - Storage::Disk::Drive{clock_rate, 300, 1} + // Bit of a hack here: drives are marginally slowed down compared to real drives + // in order to accomodate NIB files, which usually carry more data than will + // physically fit on a track once slip bits are reinserted. + // + // I don't like the coupling here. + // TODO: resolve better, somehow. + Storage::Disk::Drive{clock_rate, 295, 1}, + Storage::Disk::Drive{clock_rate, 295, 1} } { drives_[0].set_clocking_hint_observer(this); @@ -55,7 +61,12 @@ void DiskII::set_control(Control control, bool on) { if(stepper_mask_&2) direction += (((stepper_position_ - 2) + 4)&7) - 4; if(stepper_mask_&4) direction += (((stepper_position_ - 4) + 4)&7) - 4; if(stepper_mask_&8) direction += (((stepper_position_ - 6) + 4)&7) - 4; - const int bits_set = (stepper_mask_&1) + ((stepper_mask_ >> 1)&1) + ((stepper_mask_ >> 2)&1) + ((stepper_mask_ >> 3)&1); + + // TODO: when adopting C++20, replace with std::popcount. + int bits_set = stepper_mask_; + bits_set = (bits_set & 0x5) + ((bits_set >> 1) & 0x5); + bits_set = (bits_set & 0x3) + ((bits_set >> 2) & 0x3); + direction /= bits_set; // Compare to the stepper position to decide whether that pulls in the current cog notch, @@ -91,12 +102,15 @@ void DiskII::run_for(const Cycles cycles) { state_ = state_machine_[size_t(address)]; switch(state_ & 0xf) { default: shift_register_ = 0; break; // clear - case 0x8: break; // nop + + case 0x8: + case 0xc: break; // nop case 0x9: shift_register_ = uint8_t(shift_register_ << 1); break; // shift left, bringing in a zero case 0xd: shift_register_ = uint8_t((shift_register_ << 1) | 1); break; // shift left, bringing in a one - case 0xa: // shift right, bringing in write protected status + case 0xa: + case 0xe: // shift right, bringing in write protected status shift_register_ = (shift_register_ >> 1) | (is_write_protected() ? 0x80 : 0x00); // If the controller is in the sense write protect loop but the register will never change, @@ -108,14 +122,16 @@ void DiskII::run_for(const Cycles cycles) { return; } break; - case 0xb: shift_register_ = data_input_; break; // load data register from data bus + + case 0xb: + case 0xf: shift_register_ = data_input_; break; // load data register from data bus } // Currently writing? if(inputs_&input_mode) { // state_ & 0x80 should be the current level sent to the disk; // therefore transitions in that bit should become flux transitions - drives_[active_drive_].write_bit(!!((state_ ^ address) & 0x80)); + drives_[active_drive_].write_bit((state_ ^ address) & 0x80); } // TODO: surely there's a less heavyweight solution than inline updates? @@ -144,7 +160,7 @@ void DiskII::decide_clocking_preference() { // none, given that drives are not running, the shift register has already emptied or stopped and there's no flux about to be received. if(!(inputs_ & ~input_flux)) { const bool is_stuck_at_nop = - !flux_duration_ && state_machine_[(state_ & 0xf0) | inputs_ | ((shift_register_&0x80) >> 6)] == state_ && (state_ &0xf) == 0x8; + !flux_duration_ && state_machine_[(state_ & 0xf0) | inputs_ | ((shift_register_ & 0x80) >> 6)] == state_ && ((state_ & 0xf) == 0x8 || (state_ & 0xf) == 0xc); clocking_preference_ = (drive_is_sleeping_[0] && drive_is_sleeping_[1] && (!shift_register_ || is_stuck_at_nop) && (inputs_&input_flux)) @@ -159,7 +175,7 @@ void DiskII::decide_clocking_preference() { // If in sense-write-protect mode, clocking is just-in-time if the shift register hasn't yet filled with the value that // corresponds to the current write protect status. Otherwise it is none. if((inputs_ & ~input_flux) == input_command) { - clocking_preference_ = (shift_register_ == (is_write_protected() ? 0xff : 0x00)) ? ClockingHint::Preference::None : ClockingHint::Preference::JustInTime; + clocking_preference_ = ((shift_register_ == (is_write_protected() ? 0xff : 0x00)) && ((state_ & 0xf) == 0xa || (state_ & 0xf) == 0xe)) ? ClockingHint::Preference::None : ClockingHint::Preference::JustInTime; } // Announce a change if there was one. @@ -168,7 +184,7 @@ void DiskII::decide_clocking_preference() { } bool DiskII::is_write_protected() { - return !!(stepper_mask_ & 2) | drives_[active_drive_].get_is_read_only(); + return (stepper_mask_ & 2) || drives_[active_drive_].get_is_read_only(); } void DiskII::set_state_machine(const std::vector &state_machine) { @@ -289,7 +305,7 @@ int DiskII::read_address(int address) { break; } decide_clocking_preference(); - return (address & 1) ? 0xff : shift_register_; + return (address & 1) ? DidNotLoad : shift_register_; } void DiskII::set_activity_observer(Activity::Observer *observer) { diff --git a/Components/DiskII/DiskII.hpp b/Components/DiskII/DiskII.hpp index 5afbf0169..68df92c26 100644 --- a/Components/DiskII/DiskII.hpp +++ b/Components/DiskII/DiskII.hpp @@ -6,8 +6,7 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef DiskII_hpp -#define DiskII_hpp +#pragma once #include "../../ClockReceiver/ClockReceiver.hpp" #include "../../ClockReceiver/ClockingHintSource.hpp" @@ -126,5 +125,3 @@ class DiskII : }; } - -#endif /* DiskII_hpp */ diff --git a/Components/DiskII/DiskIIDrive.hpp b/Components/DiskII/DiskIIDrive.hpp index e003919aa..9de3523d4 100644 --- a/Components/DiskII/DiskIIDrive.hpp +++ b/Components/DiskII/DiskIIDrive.hpp @@ -6,13 +6,11 @@ // Copyright © 2020 Thomas Harte. All rights reserved. // -#ifndef DiskIIDrive_hpp -#define DiskIIDrive_hpp +#pragma once #include "IWM.hpp" -namespace Apple { -namespace Disk { +namespace Apple::Disk { class DiskIIDrive: public IWMDrive { public: @@ -28,6 +26,3 @@ class DiskIIDrive: public IWMDrive { }; } -} - -#endif /* DiskIIDrive_hpp */ diff --git a/Components/DiskII/IWM.cpp b/Components/DiskII/IWM.cpp index b8d4e6465..57cb5a798 100644 --- a/Components/DiskII/IWM.cpp +++ b/Components/DiskII/IWM.cpp @@ -8,16 +8,11 @@ #include "IWM.hpp" -#ifndef NDEBUG -#define NDEBUG -#endif - -#define LOG_PREFIX "[IWM] " #include "../../Outputs/Log.hpp" using namespace Apple; -namespace { +namespace { constexpr int CA0 = 1 << 0; constexpr int CA1 = 1 << 1; constexpr int CA2 = 1 << 2; @@ -27,6 +22,8 @@ namespace { constexpr int Q6 = 1 << 6; constexpr int Q7 = 1 << 7; constexpr int SEL = 1 << 8; /* This is an additional input, not available on a Disk II, with a confusingly-similar name to SELECT but a distinct purpose. */ + + Log::Logger logger; } IWM::IWM(int clock_rate) : @@ -55,7 +52,7 @@ uint8_t IWM::read(int address) { switch(state_ & (Q6 | Q7 | ENABLE)) { default: - LOG("Invalid read\n"); + logger.info().append("Invalid read\n"); return 0xff; // "Read all 1s". @@ -68,9 +65,9 @@ uint8_t IWM::read(int address) { if(data_register_ & 0x80) { data_register_ = 0; -// LOG("Reading data: " << PADHEX(2) << int(result)); +// logger.info().append("Reading data: %02x", result); } -// LOG("Reading data register: " << PADHEX(2) << int(result)); +// logger.info().append("Reading data register: %02x", result); return result; } @@ -103,7 +100,7 @@ uint8_t IWM::read(int address) { bit 6: 1 = write state (0 = underrun has occurred; 1 = no underrun so far). bit 7: 1 = write data buffer ready for data (1 = ready; 0 = busy). */ -// LOG("Reading write handshake: " << PADHEX(2) << int(0x3f | write_handshake_)); +// logger.info().append("Reading write handshake: %02x", 0x3f | write_handshake_); return 0x3f | write_handshake_; } @@ -134,9 +131,9 @@ void IWM::write(int address, uint8_t input) { // TEMPORARY. To test for the unimplemented mode. if(input&0x2) { - LOG("Switched to asynchronous mode"); + logger.info().append("Switched to asynchronous mode"); } else { - LOG("Switched to synchronous mode"); + logger.info().append("Switched to synchronous mode"); } switch(mode_ & 0x18) { @@ -145,8 +142,8 @@ void IWM::write(int address, uint8_t input) { case 0x10: bit_length_ = Cycles(32); break; // slow mode, 8Mhz case 0x18: bit_length_ = Cycles(16); break; // fast mode, 8Mhz } - LOG("Mode is now " << PADHEX(2) << int(mode_)); - LOG("New bit length is " << std::dec << bit_length_.as_integral()); + logger.info().append("Mode is now %02x", mode_); + logger.info().append("New bit length is %d", bit_length_.as()); break; case Q7|Q6|ENABLE: // Write data register. @@ -220,7 +217,7 @@ void IWM::set_select(bool enabled) { } void IWM::push_drive_state() { - if(drives_[active_drive_]) { + if(drives_[active_drive_]) { const uint8_t drive_control_lines = ((state_ & CA0) ? IWMDrive::CA0 : 0) | ((state_ & CA1) ? IWMDrive::CA1 : 0) | @@ -260,7 +257,7 @@ void IWM::run_for(const Cycles cycles) { drives_[active_drive_]->run_for(Cycles(1)); ++cycles_since_shift_; if(cycles_since_shift_ == bit_length_ + error_margin) { -// LOG("Shifting 0 at " << std::dec << cycles_since_shift_.as_integral()); +// logger.info().append("Shifting 0 at %d ", cycles_since_shift_.as()); propose_shift(0); } } @@ -294,11 +291,11 @@ void IWM::run_for(const Cycles cycles) { if(!(write_handshake_ & 0x80)) { shift_register_ = next_output_; output_bits_remaining_ = 8; -// LOG("Next byte: " << PADHEX(2) << int(shift_register_)); +// logger.info().append("Next byte: %02x", shift_register_); } else { write_handshake_ &= ~0x40; if(drives_[active_drive_]) drives_[active_drive_]->end_writing(); - LOG("Overrun; done."); + logger.info().append("Overrun; done."); output_bits_remaining_ = 1; } @@ -354,7 +351,7 @@ void IWM::select_shift_mode() { shift_register_ = next_output_; write_handshake_ |= 0x80 | 0x40; output_bits_remaining_ = 8; - LOG("Seeding output with " << PADHEX(2) << int(shift_register_)); + logger.info().append("Seeding output with %02x", shift_register_); } } @@ -368,7 +365,7 @@ void IWM::process_event(const Storage::Disk::Drive::Event &event) { switch(event.type) { case Storage::Disk::Track::Event::IndexHole: return; case Storage::Disk::Track::Event::FluxTransition: -// LOG("Shifting 1 at " << std::dec << cycles_since_shift_.as_integral()); +// logger.info().append("Shifting 1 at %d", cycles_since_shift_.as()); propose_shift(1); break; } @@ -377,8 +374,8 @@ void IWM::process_event(const Storage::Disk::Drive::Event &event) { void IWM::propose_shift(uint8_t bit) { // TODO: synchronous mode. -// LOG("Shifting at " << std::dec << cycles_since_shift_.as_integral()); -// LOG("Shifting input"); +// logger.info().append("Shifting at %d", cycles_since_shift_.as()); +// logger.info().append("Shifting input"); // See above for text from the IWM patent, column 7, around line 35 onwards. // The error_margin here implements the 'before' part of that contract. @@ -393,7 +390,7 @@ void IWM::propose_shift(uint8_t bit) { shift_register_ = uint8_t((shift_register_ << 1) | bit); if(shift_register_ & 0x80) { -// if(data_register_ & 0x80) LOG("Byte missed"); +// if(data_register_ & 0x80) logger.info().append("Byte missed"); data_register_ = shift_register_; shift_register_ = 0; } diff --git a/Components/DiskII/IWM.hpp b/Components/DiskII/IWM.hpp index ecb57f97a..347fa75bf 100644 --- a/Components/DiskII/IWM.hpp +++ b/Components/DiskII/IWM.hpp @@ -6,8 +6,7 @@ // Copyright © 2019 Thomas Harte. All rights reserved. // -#ifndef IWM_hpp -#define IWM_hpp +#pragma once #include "../../Activity/Observer.hpp" @@ -119,7 +118,4 @@ class IWM: void select_shift_mode(); }; - } - -#endif /* IWM_hpp */ diff --git a/Components/DiskII/MacintoshDoubleDensityDrive.hpp b/Components/DiskII/MacintoshDoubleDensityDrive.hpp index 3c6bf5b8f..be0f2cc6e 100644 --- a/Components/DiskII/MacintoshDoubleDensityDrive.hpp +++ b/Components/DiskII/MacintoshDoubleDensityDrive.hpp @@ -6,13 +6,11 @@ // Copyright © 2019 Thomas Harte. All rights reserved. // -#ifndef MacintoshDoubleDensityDrive_hpp -#define MacintoshDoubleDensityDrive_hpp +#pragma once #include "IWM.hpp" -namespace Apple { -namespace Macintosh { +namespace Apple::Macintosh { class DoubleDensityDrive: public IWMDrive { public: @@ -48,6 +46,3 @@ class DoubleDensityDrive: public IWMDrive { }; } -} - -#endif /* MacintoshDoubleDensityDrive_hpp */ diff --git a/Components/KonamiSCC/KonamiSCC.hpp b/Components/KonamiSCC/KonamiSCC.hpp index b1db2b17d..9b16e8a1d 100644 --- a/Components/KonamiSCC/KonamiSCC.hpp +++ b/Components/KonamiSCC/KonamiSCC.hpp @@ -6,8 +6,7 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef KonamiSCC_hpp -#define KonamiSCC_hpp +#pragma once #include "../../Outputs/Speaker/Implementation/SampleSource.hpp" #include "../../Concurrency/AsyncTaskQueue.hpp" @@ -70,5 +69,3 @@ class SCC: public ::Outputs::Speaker::SampleSource { }; } - -#endif /* KonamiSCC_hpp */ diff --git a/Components/OPx/Implementation/EnvelopeGenerator.hpp b/Components/OPx/Implementation/EnvelopeGenerator.hpp index 3d7b80c19..e9f592a5d 100644 --- a/Components/OPx/Implementation/EnvelopeGenerator.hpp +++ b/Components/OPx/Implementation/EnvelopeGenerator.hpp @@ -6,15 +6,13 @@ // Copyright © 2020 Thomas Harte. All rights reserved. // -#ifndef EnvelopeGenerator_h -#define EnvelopeGenerator_h +#pragma once #include #include #include "LowFrequencyOscillator.hpp" -namespace Yamaha { -namespace OPL { +namespace Yamaha::OPL { /*! Models an OPL-style envelope generator. @@ -259,6 +257,3 @@ template class EnvelopeGenerator }; } -} - -#endif /* EnvelopeGenerator_h */ diff --git a/Components/OPx/Implementation/KeyLevelScaler.hpp b/Components/OPx/Implementation/KeyLevelScaler.hpp index a86995bdf..fe4e59243 100644 --- a/Components/OPx/Implementation/KeyLevelScaler.hpp +++ b/Components/OPx/Implementation/KeyLevelScaler.hpp @@ -6,11 +6,9 @@ // Copyright © 2020 Thomas Harte. All rights reserved. // -#ifndef KeyLevelScaler_h -#define KeyLevelScaler_h +#pragma once -namespace Yamaha { -namespace OPL { +namespace Yamaha::OPL { template class KeyLevelScaler { public: @@ -51,8 +49,4 @@ template class KeyLevelScaler { int shift_ = 0; }; - } -} - -#endif /* KeyLevelScaler_h */ diff --git a/Components/OPx/Implementation/LowFrequencyOscillator.hpp b/Components/OPx/Implementation/LowFrequencyOscillator.hpp index a3d852e67..bc8877ef8 100644 --- a/Components/OPx/Implementation/LowFrequencyOscillator.hpp +++ b/Components/OPx/Implementation/LowFrequencyOscillator.hpp @@ -6,13 +6,11 @@ // Copyright © 2020 Thomas Harte. All rights reserved. // -#ifndef LowFrequencyOscillator_hpp -#define LowFrequencyOscillator_hpp +#pragma once #include "../../../Numeric/LFSR.hpp" -namespace Yamaha { -namespace OPL { +namespace Yamaha::OPL { /*! Models the output of the OPL low-frequency oscillator, which provides a couple of optional fixed-frequency @@ -54,7 +52,7 @@ class LowFrequencyOscillator { /// Updartes the LFSR output. Should be called at the input clock rate. void update_lfsr() { - lfsr = noise_source_.next(); + lfsr = noise_source_.next(); } private: @@ -63,6 +61,3 @@ class LowFrequencyOscillator { }; } -} - -#endif /* LowFrequencyOscillator_hpp */ diff --git a/Components/OPx/Implementation/OPLBase.hpp b/Components/OPx/Implementation/OPLBase.hpp index 8183d4287..a3067395c 100644 --- a/Components/OPx/Implementation/OPLBase.hpp +++ b/Components/OPx/Implementation/OPLBase.hpp @@ -6,14 +6,12 @@ // Copyright © 2020 Thomas Harte. All rights reserved. // -#ifndef OPLBase_h -#define OPLBase_h +#pragma once #include "../../../Outputs/Speaker/Implementation/SampleSource.hpp" #include "../../../Concurrency/AsyncTaskQueue.hpp" -namespace Yamaha { -namespace OPL { +namespace Yamaha::OPL { template class OPLBase: public ::Outputs::Speaker::SampleSource { public: @@ -35,6 +33,3 @@ template class OPLBase: public ::Outputs::Speaker::SampleSource }; } -} - -#endif /* OPLBase_h */ diff --git a/Components/OPx/Implementation/PhaseGenerator.hpp b/Components/OPx/Implementation/PhaseGenerator.hpp index 89ca27ebd..f1a17f076 100644 --- a/Components/OPx/Implementation/PhaseGenerator.hpp +++ b/Components/OPx/Implementation/PhaseGenerator.hpp @@ -6,15 +6,13 @@ // Copyright © 2020 Thomas Harte. All rights reserved. // -#ifndef PhaseGenerator_h -#define PhaseGenerator_h +#pragma once #include #include "LowFrequencyOscillator.hpp" #include "Tables.hpp" -namespace Yamaha { -namespace OPL { +namespace Yamaha::OPL { /*! Models an OPL-style phase generator of templated precision; having been told its period ('f-num'), octave ('block') and @@ -109,7 +107,7 @@ template class PhaseGenerator { } private: - static constexpr int precision_shift = 1 + precision; + static constexpr int precision_shift = 1 + precision; int phase_ = 0; @@ -120,6 +118,3 @@ template class PhaseGenerator { }; } -} - -#endif /* PhaseGenerator_h */ diff --git a/Components/OPx/Implementation/Tables.hpp b/Components/OPx/Implementation/Tables.hpp index d50f2c391..bcdbe6593 100644 --- a/Components/OPx/Implementation/Tables.hpp +++ b/Components/OPx/Implementation/Tables.hpp @@ -6,11 +6,9 @@ // Copyright © 2020 Thomas Harte. All rights reserved. // -#ifndef Tables_hpp -#define Tables_hpp +#pragma once -namespace Yamaha { -namespace OPL { +namespace Yamaha::OPL { /* These are the OPL's built-in log-sin and exponentiation tables, as recovered by @@ -52,7 +50,7 @@ struct LogSign { @returns Negative log sin of x, assuming a 1024-unit circle. */ constexpr LogSign negative_log_sin(int x) { - /// Defines the first quadrant of 1024-unit negative log to the base two of sine (that conveniently misses sin(0)). + /// Defines the first quadrant of 1024-unit negative log to the base two of sine (that conveniently misses sin(0)). /// /// Expected branchless usage for a full 1024 unit output: /// @@ -222,6 +220,3 @@ inline int LogSign::level(int fractional) const { } } -} - -#endif /* Tables_hpp */ diff --git a/Components/OPx/Implementation/WaveformGenerator.hpp b/Components/OPx/Implementation/WaveformGenerator.hpp index 381fe7eea..8576e3c2c 100644 --- a/Components/OPx/Implementation/WaveformGenerator.hpp +++ b/Components/OPx/Implementation/WaveformGenerator.hpp @@ -6,14 +6,12 @@ // Copyright © 2020 Thomas Harte. All rights reserved. // -#ifndef WaveformGenerator_h -#define WaveformGenerator_h +#pragma once #include "Tables.hpp" #include "LowFrequencyOscillator.hpp" -namespace Yamaha { -namespace OPL { +namespace Yamaha::OPL { enum class Waveform { Sine, HalfSine, AbsSine, PulseSine @@ -87,6 +85,3 @@ template class WaveformGenerator { }; } -} - -#endif /* WaveformGenerator_h */ diff --git a/Components/OPx/OPLL.hpp b/Components/OPx/OPLL.hpp index 530d84e77..36bb0b51d 100644 --- a/Components/OPx/OPLL.hpp +++ b/Components/OPx/OPLL.hpp @@ -6,8 +6,7 @@ // Copyright © 2020 Thomas Harte. All rights reserved. // -#ifndef OPLL_hpp -#define OPLL_hpp +#pragma once #include "Implementation/OPLBase.hpp" #include "Implementation/EnvelopeGenerator.hpp" @@ -18,8 +17,7 @@ #include -namespace Yamaha { -namespace OPL { +namespace Yamaha::OPL { class OPLL: public OPLBase { public: @@ -126,6 +124,3 @@ class OPLL: public OPLBase { }; } -} - -#endif /* OPLL_hpp */ diff --git a/Components/RP5C01/RP5C01.cpp b/Components/RP5C01/RP5C01.cpp new file mode 100644 index 000000000..3caf220b8 --- /dev/null +++ b/Components/RP5C01/RP5C01.cpp @@ -0,0 +1,323 @@ +// +// RP5C01.cpp +// Clock Signal +// +// Created by Thomas Harte on 14/01/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#include "RP5C01.hpp" + +#include "../../Numeric/NumericCoder.hpp" + +#include + +using namespace Ricoh::RP5C01; + +RP5C01::RP5C01(HalfCycles clock_rate) : clock_rate_(clock_rate) { + // Seed internal clock. + std::time_t now = std::time(NULL); + std::tm *time_date = std::localtime(&now); + + seconds_ = + time_date->tm_sec + + time_date->tm_min * 60 + + time_date->tm_hour * 60 * 60; + + day_of_the_week_ = time_date->tm_wday; + day_ = time_date->tm_mday; + month_ = time_date->tm_mon; + year_ = (time_date->tm_year + 20) % 100; // This is probably MSX specific; rethink if/when other machines use this chip. + leap_year_ = time_date->tm_year % 4; +} + +void RP5C01::run_for(HalfCycles cycles) { + sub_seconds_ += cycles; + + // Guess: this happens so rarely (i.e. once a second, ordinarily) that + // it's not worth worrying about the branch prediction consequences. + // + // ... and ditto all the conditionals below, which will be very rarely reached. + if(sub_seconds_ < clock_rate_) { + return; + } + const auto elapsed_seconds = int(sub_seconds_.as_integral() / clock_rate_.as_integral()); + sub_seconds_ %= clock_rate_; + + // Update time within day. + seconds_ += elapsed_seconds; + + constexpr int day_length = 60 * 60 * 24; + if(seconds_ < day_length) { + return; + } + const int elapsed_days = seconds_ / day_length; + seconds_ %= day_length; + + // Day of the week doesn't aggregate upwards. + day_of_the_week_ = (day_of_the_week_ + elapsed_days) % 7; + + // Assumed for now: day and month run from 0. + // A leap year count of 0 implies a leap year. + // TODO: verify. + day_ += elapsed_days; + while(true) { + int month_length = 1; + switch(month_) { + default: + case 0: month_length = 31; break; + case 1: month_length = 28 + !leap_year_; break; + case 2: month_length = 31; break; + case 3: month_length = 30; break; + case 4: month_length = 31; break; + case 5: month_length = 30; break; + case 6: month_length = 31; break; + case 7: month_length = 31; break; + case 8: month_length = 30; break; + case 9: month_length = 31; break; + case 10: month_length = 30; break; + case 11: month_length = 31; break; + } + + if(day_ < month_length) { + return; + } + + day_ -= month_length; + ++month_; + + if(month_ == 12) { + month_ = 0; + year_ = (year_ + 1) % 100; + leap_year_ = (leap_year_ + 1) & 3; + } + } +} + +namespace { + +constexpr int Reg(int mode, int address) { + return address | mode << 4; +} + +constexpr int PM = 1 << 4; + +constexpr int twenty_four_to_twelve(int hours) { + switch(hours) { + default: return (hours % 12) + (hours > 12 ? PM : 0); + case 0: return 12; + case 12: return 12 | PM; + } +} + +constexpr int twelve_to_twenty_four(int hours) { + hours = (hours & 0xf) + (hours & PM ? 12 : 0); + switch(hours) { + default: break; + case 24: return 12; + case 12: return 0; + } + return hours; +} + +using SecondEncoder = Numeric::NumericCoder< + 10, 6, // Seconds. + 10, 6, // Minutes. + 24 // Hours +>; +using TwoDigitEncoder = Numeric::NumericCoder<10, 10>; + +} + +/// Performs a write of @c value to @c address. +void RP5C01::write(int address, uint8_t value) { + address &= 0xf; + value &= 0xf; + + // Handle potential RAM accesses. + if(address < 0xd && mode_ >= 2) { + address += mode_ == 3 ? 13 : 0; + ram_[size_t(address)] = value & 0xf; + return; + } + + switch(Reg(mode_, address)) { + default: break; + + // Seconds. + case Reg(0, 0x00): SecondEncoder::encode<0>(seconds_, value); break; + case Reg(0, 0x01): SecondEncoder::encode<1>(seconds_, value); break; + + // Minutes. + case Reg(0, 0x02): SecondEncoder::encode<2>(seconds_, value); break; + case Reg(0, 0x03): SecondEncoder::encode<3>(seconds_, value); break; + + // Hours. + case Reg(0, 0x04): + case Reg(0, 0x05): { + int hours = SecondEncoder::decode<4>(seconds_); + if(!twentyfour_hour_clock_) { + hours = twenty_four_to_twelve(hours); + } + if(address == 0x4) { + TwoDigitEncoder::encode<0>(hours, value); + } else { + TwoDigitEncoder::encode<1>(hours, value & 3); + } + if(!twentyfour_hour_clock_) { + hours = twelve_to_twenty_four(hours); + } + SecondEncoder::encode<4>(seconds_, hours); + } break; + + // Day of the week. + case Reg(0, 0x06): day_of_the_week_ = value % 7; break; + + // Day. + case Reg(0, 0x07): TwoDigitEncoder::encode<0>(day_, value); break; + case Reg(0, 0x08): TwoDigitEncoder::encode<1>(day_, value & 3); break; + + // Month. + case Reg(0, 0x09): TwoDigitEncoder::encode<0>(month_, (value - 1)); break; + case Reg(0, 0x0a): TwoDigitEncoder::encode<1>(month_, (value - 1) & 1); break; + + // Year. + case Reg(0, 0x0b): TwoDigitEncoder::encode<0>(year_, value); break; + case Reg(0, 0x0c): TwoDigitEncoder::encode<1>(year_, value); break; + + // TODO: alarm minutes. + case Reg(1, 0x02): + case Reg(1, 0x03): break; + + // TODO: alarm hours. + case Reg(1, 0x04): + case Reg(1, 0x05): break; + + // TODO: alarm day-of-the-week. + case Reg(1, 0x06): break; + + // TODO: alarm day. + case Reg(1, 0x07): + case Reg(1, 0x08): break; + + // 24/12-hour clock. + case Reg(1, 0x0a): + twentyfour_hour_clock_ = value & 1; + break; + + // Lead-year counter. + case Reg(1, 0x0b): + leap_year_ = value & 3; + break; + + // + // Registers D–F don't depend on the mode. + // + + case Reg(0, 0xd): case Reg(1, 0xd): case Reg(2, 0xd): case Reg(3, 0xd): + timer_enabled_ = value & 0x8; + alarm_enabled_ = value & 0x4; + mode_ = value & 0x3; + break; + case Reg(0, 0xe): case Reg(1, 0xe): case Reg(2, 0xe): case Reg(3, 0xe): + // Test register; unclear what is supposed to happen. + break; + case Reg(0, 0xf): case Reg(1, 0xf): case Reg(2, 0xf): case Reg(3, 0xf): + one_hz_on_ = !(value & 0x8); + sixteen_hz_on_ = !(value & 0x4); + // TODO: b0 = alarm reset; b1 = timer reset. + break; + } + +} + +uint8_t RP5C01::read(int address) { + address &= 0xf; + + if(address < 0xd && mode_ >= 2) { + address += mode_ == 3 ? 13 : 0; + return 0xf0 | ram_[size_t(address)]; + } + + int value = 0xf; + switch(Reg(mode_, address)) { + // Second. + case Reg(0, 0x00): value = SecondEncoder::decode<0>(seconds_); break; + case Reg(0, 0x01): value = SecondEncoder::decode<1>(seconds_); break; + + // Minute. + case Reg(0, 0x02): value = SecondEncoder::decode<2>(seconds_); break; + case Reg(0, 0x03): value = SecondEncoder::decode<3>(seconds_); break; + + // Hour. + case Reg(0, 0x04): + case Reg(0, 0x05): { + int hours = SecondEncoder::decode<4>(seconds_); + if(!twentyfour_hour_clock_) { + hours = twenty_four_to_twelve(hours); + } + if(address == 0x4) { + value = TwoDigitEncoder::decode<0>(hours); + } else { + value = TwoDigitEncoder::decode<1>(hours); + } + } break; + + // Day-of-the-week. + case Reg(0, 0x06): value = day_of_the_week_; break; + + // Day. + case Reg(0, 0x07): value = TwoDigitEncoder::decode<0>(day_); break; + case Reg(0, 0x08): value = TwoDigitEncoder::decode<1>(day_); break; + + // Month. + case Reg(0, 0x09): value = TwoDigitEncoder::decode<0>(month_ + 1); break; + case Reg(0, 0x0a): value = TwoDigitEncoder::decode<1>(month_ + 1); break; + + // Year. + case Reg(0, 0x0b): value = TwoDigitEncoder::decode<0>(year_); break; + case Reg(0, 0x0c): value = TwoDigitEncoder::decode<1>(year_); break; + + // TODO: alarm minutes. + case Reg(1, 0x02): + case Reg(1, 0x03): break; + + // TODO: alarm hours. + case Reg(1, 0x04): + case Reg(1, 0x05): break; + + // TODO: alarm day-of-the-week. + case Reg(1, 0x06): break; + + // TODO: alarm day. + case Reg(1, 0x07): + case Reg(1, 0x08): break; + + // 12/24-hour clock. + case Reg(1, 0x0a): value = twentyfour_hour_clock_; break; + + // Leap year. + case Reg(1, 0x0b): value = leap_year_; break; + + // + // Registers D–F don't depend on the mode. + // + + case Reg(0, 0xd): case Reg(1, 0xd): case Reg(2, 0xd): case Reg(3, 0xd): + value = + (timer_enabled_ ? 0x8 : 0x0) | + (alarm_enabled_ ? 0x4 : 0x0) | + mode_; + break; + case Reg(0, 0xe): case Reg(1, 0xe): case Reg(2, 0xe): case Reg(3, 0xe): + // Test register; unclear what is supposed to happen. + break; + case Reg(0, 0xf): case Reg(1, 0xf): case Reg(2, 0xf): case Reg(3, 0xf): + value = + (one_hz_on_ ? 0x0 : 0x8) | + (sixteen_hz_on_ ? 0x0 : 0x4); + break; + } + + return uint8_t(0xf0 | value); +} diff --git a/Components/RP5C01/RP5C01.hpp b/Components/RP5C01/RP5C01.hpp new file mode 100644 index 000000000..5d0574cf6 --- /dev/null +++ b/Components/RP5C01/RP5C01.hpp @@ -0,0 +1,56 @@ +// +// RP5C01.hpp +// Clock Signal +// +// Created by Thomas Harte on 14/01/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "../../ClockReceiver/ClockReceiver.hpp" + +#include +#include + +namespace Ricoh::RP5C01 { + +class RP5C01 { + public: + RP5C01(HalfCycles clock_rate); + + /// @returns the result of a read from @c address. + uint8_t read(int address); + + /// Performs a write of @c value to @c address. + void write(int address, uint8_t value); + + /// Advances time. + void run_for(HalfCycles); + + private: + std::array ram_; + + HalfCycles sub_seconds_; + const HalfCycles clock_rate_; + + // Contains the seconds, minutes and hours fields. + int seconds_ = 0; + + // Calendar entries. + int day_of_the_week_ = 0; + int day_ = 0; + int month_ = 0; + int year_ = 0; + int leap_year_ = 0; + + // Other flags. + bool timer_enabled_ = false; + bool alarm_enabled_ = false; + int mode_ = 0; + bool one_hz_on_ = false; + bool sixteen_hz_on_ = false; + bool twentyfour_hour_clock_ = true; +}; + +} diff --git a/Components/SN76489/SN76489.hpp b/Components/SN76489/SN76489.hpp index 877c71354..d419692f8 100644 --- a/Components/SN76489/SN76489.hpp +++ b/Components/SN76489/SN76489.hpp @@ -6,8 +6,7 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef SN76489_hpp -#define SN76489_hpp +#pragma once #include "../../Outputs/Speaker/Implementation/SampleSource.hpp" #include "../../Concurrency/AsyncTaskQueue.hpp" @@ -65,5 +64,3 @@ class SN76489: public Outputs::Speaker::SampleSource { }; } - -#endif /* SN76489_hpp */ diff --git a/Components/Serial/Line.cpp b/Components/Serial/Line.cpp index 62ce110a0..d6eb875eb 100644 --- a/Components/Serial/Line.cpp +++ b/Components/Serial/Line.cpp @@ -129,7 +129,7 @@ bool Line::read() const { } template -void Line::set_read_delegate(ReadDelegate *delegate, Storage::Time bit_length) { +void Line::set_read_delegate(ReadDelegate *delegate, [[maybe_unused]] Storage::Time bit_length) { read_delegate_ = delegate; if constexpr (!include_clock) { assert(bit_length > Storage::Time(0)); diff --git a/Components/Serial/Line.hpp b/Components/Serial/Line.hpp index bbd68f9ea..483815c29 100644 --- a/Components/Serial/Line.hpp +++ b/Components/Serial/Line.hpp @@ -6,8 +6,7 @@ // Copyright © 2019 Thomas Harte. All rights reserved. // -#ifndef SerialPort_hpp -#define SerialPort_hpp +#pragma once #include #include "../../Storage/Storage.hpp" @@ -143,5 +142,3 @@ class Port { }; } - -#endif /* SerialPort_hpp */ diff --git a/Concurrency/AsyncTaskQueue.hpp b/Concurrency/AsyncTaskQueue.hpp index f0de9f640..594438446 100644 --- a/Concurrency/AsyncTaskQueue.hpp +++ b/Concurrency/AsyncTaskQueue.hpp @@ -6,8 +6,7 @@ // Copyright 2016 Thomas Harte. All rights reserved. // -#ifndef AsyncTaskQueue_hpp -#define AsyncTaskQueue_hpp +#pragma once #include #include @@ -120,7 +119,7 @@ template class QuickbootOption { }; } - -#endif /* StandardOptions_hpp */ diff --git a/Inputs/Joystick.hpp b/Inputs/Joystick.hpp index 57ee46824..9460568d8 100644 --- a/Inputs/Joystick.hpp +++ b/Inputs/Joystick.hpp @@ -6,8 +6,7 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef Joystick_hpp -#define Joystick_hpp +#pragma once #include #include @@ -234,5 +233,3 @@ class ConcreteJoystick: public Joystick { }; } - -#endif /* Joystick_hpp */ diff --git a/Inputs/Keyboard.cpp b/Inputs/Keyboard.cpp index f58bca83a..7abdb87e5 100644 --- a/Inputs/Keyboard.cpp +++ b/Inputs/Keyboard.cpp @@ -21,7 +21,7 @@ Keyboard::Keyboard(const std::set &essential_modifiers) : essential_modifie Keyboard::Keyboard(const std::set &observed_keys, const std::set &essential_modifiers) : observed_keys_(observed_keys), essential_modifiers_(essential_modifiers), is_exclusive_(false) {} -bool Keyboard::set_key_pressed(Key key, char, bool is_pressed) { +bool Keyboard::set_key_pressed(Key key, char, bool is_pressed, bool) { const size_t key_offset = size_t(key); if(key_offset >= key_states_.size()) { key_states_.resize(key_offset+1, false); diff --git a/Inputs/Keyboard.hpp b/Inputs/Keyboard.hpp index 0b4a4120c..8b86fac7f 100644 --- a/Inputs/Keyboard.hpp +++ b/Inputs/Keyboard.hpp @@ -6,8 +6,7 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef Inputs_Keyboard_hpp -#define Inputs_Keyboard_hpp +#pragma once #include #include @@ -51,7 +50,7 @@ class Keyboard { // Host interface. /// @returns @c true if the key press affects the machine; @c false otherwise. - virtual bool set_key_pressed(Key key, char value, bool is_pressed); + virtual bool set_key_pressed(Key key, char value, bool is_pressed, bool is_repeat); virtual void reset_all_keys(); /// @returns a set of all Keys that this keyboard responds to. @@ -90,5 +89,3 @@ class Keyboard { }; } - -#endif /* Inputs_Keyboard_hpp */ diff --git a/Inputs/Mouse.hpp b/Inputs/Mouse.hpp index 5ea3ec8b8..9772abfd3 100644 --- a/Inputs/Mouse.hpp +++ b/Inputs/Mouse.hpp @@ -6,8 +6,7 @@ // Copyright © 2019 Thomas Harte. All rights reserved. // -#ifndef Mouse_h -#define Mouse_h +#pragma once namespace Inputs { @@ -43,5 +42,3 @@ class Mouse { }; } - -#endif /* Mouse_h */ diff --git a/Inputs/QuadratureMouse/QuadratureMouse.hpp b/Inputs/QuadratureMouse/QuadratureMouse.hpp index b1b596a79..78d52420d 100644 --- a/Inputs/QuadratureMouse/QuadratureMouse.hpp +++ b/Inputs/QuadratureMouse/QuadratureMouse.hpp @@ -6,8 +6,7 @@ // Copyright © 2019 Thomas Harte. All rights reserved. // -#ifndef QuadratureMouse_hpp -#define QuadratureMouse_hpp +#pragma once #include "../Mouse.hpp" #include @@ -119,5 +118,3 @@ class QuadratureMouse: public Mouse { }; } - -#endif /* QuadratureMouse_hpp */ diff --git a/InstructionSets/6809/OperationMapper.hpp b/InstructionSets/6809/OperationMapper.hpp new file mode 100644 index 000000000..362bd08f8 --- /dev/null +++ b/InstructionSets/6809/OperationMapper.hpp @@ -0,0 +1,240 @@ +// +// OperationMapper.hpp +// Clock Signal +// +// Created by Thomas Harte on 17/03/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +// Cf. https://techheap.packetizer.com/processors/6809/6809Instructions.html +// +// Subject to corrections: +// +// * CWAI and the pushes and pulls at 0x3x are immediate, not inherent. +namespace InstructionSet::M6809 { + +enum class AddressingMode { + Illegal, + + Inherent, + Immediate, + Direct, + Relative, // TODO: is it worth breaking this into 8- and 16-bit versions? + Variant, + Indexed, + Extended, +}; + +enum class Operation { + None, + + SUBB, CMPB, SBCB, ADDD, ANDB, BITB, LDB, STB, + EORB, ADCB, ORB, ADDB, LDD, STD, LDU, STU, + SUBA, CMPA, SBCA, SUBD, ANDA, BITA, LDA, STA, + EORA, ADCA, ORA, ADDA, CMPX, JSR, LDX, STX, + BSR, + + NEG, COM, LSR, ROR, ASR, + LSL, ROL, DEC, INC, TST, JMP, CLR, + NEGA, COMA, LSRA, RORA, ASRA, + LSLA, ROLA, DECA, INCA, TSTA, CLRA, + NEGB, COMB, LSRB, RORB, ASRB, + LSLB, ROLB, DECB, INCB, TSTB, CLRB, + + LEAX, LEAY, LEAS, LEAU, + PSHS, PULS, PSHU, PULU, + RTS, ABX, RTI, + CWAI, MUL, RESET, SWI, + + BRA, BRN, BHI, BLS, BCC, BCS, BNE, BEQ, + BVC, BVS, BPL, BMI, BGE, BLT, BGT, BLE, + + Page1, Page2, NOP, SYNC, LBRA, LBSR, + DAA, ORCC, ANDCC, SEX, EXG, TFR, + + LBRN, LBHI, LBLS, LBCC, LBCS, LBNE, LBEQ, + LBVC, LBVS, LBPL, LBMI, LBGE, LBLT, LBGT, LBLE, + + SWI2, CMPD, CMPY, LDY, STY, LDS, STS, + + SWI3, CMPU, CMPS, +}; + +enum class Page { + Page0, Page1, Page2, +}; + +/*! + Calls @c scheduler.schedule to describe the instruction + defined by opcode @c i on page @c page. +*/ +template struct OperationMapper { + template void dispatch(SchedulerT &scheduler); +}; + +template <> +template void OperationMapper::dispatch(SchedulerT &s) { + using AM = AddressingMode; + using O = Operation; + + constexpr auto upper = (i >> 4) & 0xf; + constexpr auto lower = (i >> 0) & 0xf; + + constexpr AddressingMode modes[] = { + AM::Immediate, AM::Direct, AM::Indexed, AM::Extended + }; + constexpr AddressingMode mode = modes[(i >> 4) & 3]; + + switch(upper) { + default: break; + + case 0x1: { + constexpr Operation operations[] = { + O::Page1, O::Page2, O::NOP, O::SYNC, O::None, O::None, O::LBRA, O::LBSR, + O::None, O::DAA, O::ORCC, O::None, O::ANDCC, O::SEX, O::EXG, O::TFR, + }; + constexpr AddressingMode modes[] = { + AM::Variant, AM::Variant, AM::Inherent, AM::Inherent, + AM::Illegal, AM::Illegal, AM::Relative, AM::Relative, + AM::Illegal, AM::Inherent, AM::Immediate, AM::Illegal, + AM::Immediate, AM::Inherent, AM::Inherent, AM::Inherent, + }; + s.template schedule(); + } break; + case 0x2: { + constexpr Operation operations[] = { + O::BRA, O::BRN, O::BHI, O::BLS, O::BCC, O::BCS, O::BNE, O::BEQ, + O::BVC, O::BVS, O::BPL, O::BMI, O::BGE, O::BLT, O::BGT, O::BLE, + }; + s.template schedule(); + } break; + case 0x3: { + constexpr Operation operations[] = { + O::LEAX, O::LEAY, O::LEAS, O::LEAU, O::PSHS, O::PULS, O::PSHU, O::PULU, + O::None, O::RTS, O::ABX, O::RTI, O::CWAI, O::MUL, O::RESET, O::SWI, + }; + constexpr auto op = operations[lower]; + switch(lower) { + case 0x0: case 0x1: case 0x2: case 0x3: + s.template schedule(); + break; + case 0x4: case 0x5: case 0x6: case 0x7: case 0xc: + s.template schedule(); + break; + case 0x8: + s.template schedule(); + break; + default: + s.template schedule(); + break; + } + } break; + case 0x4: { + constexpr Operation operations[] = { + O::NEGA, O::None, O::None, O::COMA, O::LSRA, O::None, O::RORA, O::ASRA, + O::LSLA, O::ROLA, O::DECA, O::None, O::INCA, O::TSTA, O::None, O::CLRA, + }; + constexpr auto op = operations[lower]; + s.template schedule(); + } break; + case 0x5: { + constexpr Operation operations[] = { + O::NEGB, O::None, O::None, O::COMB, O::LSRB, O::None, O::RORB, O::ASRB, + O::LSLB, O::ROLB, O::DECB, O::None, O::INCB, O::TSTB, O::None, O::CLRB, + }; + constexpr auto op = operations[lower]; + s.template schedule(); + } break; + case 0x0: case 0x6: case 0x7: { + constexpr Operation operations[] = { + O::NEG, O::None, O::None, O::COM, O::LSR, O::None, O::ROR, O::ASR, + O::LSL, O::ROL, O::DEC, O::None, O::INC, O::TST, O::JMP, O::CLR, + }; + constexpr auto op = operations[lower]; + s.template schedule(); + } break; + case 0x8: case 0x9: case 0xa: case 0xb: { + constexpr Operation operations[] = { + O::SUBA, O::CMPA, O::SBCA, O::SUBD, O::ANDA, O::BITA, O::LDA, O::STA, + O::EORA, O::ADCA, O::ORA, O::ADDA, O::CMPX, O::JSR, O::LDX, O::STX, + }; + if(i == 0x8d) s.template schedule(); + else s.template schedule(); + } break; + case 0xc: case 0xd: case 0xe: case 0xf: { + constexpr Operation operations[] = { + O::SUBB, O::CMPB, O::SBCB, O::ADDD, O::ANDB, O::BITB, O::LDB, O::STB, + O::EORB, O::ADCB, O::ORB, O::ADDB, O::LDD, O::STD, O::LDU, O::STU, + }; + s.template schedule(); + } break; + } +} + +template <> +template void OperationMapper::dispatch(SchedulerT &s) { + using AM = AddressingMode; + using O = Operation; + + constexpr AddressingMode modes[] = { + AM::Immediate, AM::Direct, AM::Indexed, AM::Extended + }; + constexpr auto mode = modes[(i >> 4) & 3]; + + if constexpr (i >= 0x21 && i < 0x30) { + constexpr Operation operations[] = { + O::LBRN, O::LBHI, O::LBLS, O::LBCC, O::LBCS, O::LBNE, O::LBEQ, + O::LBVC, O::LBVS, O::LBPL, O::LBMI, O::LBGE, O::LBLT, O::LBGT, O::LBLE, + }; + s.template schedule(); + } else switch(i) { + default: s.template schedule(); break; + case 0x3f: s.template schedule(); break; + + case 0x83: case 0x93: case 0xa3: case 0xb3: + s.template schedule(); + break; + case 0x8c: case 0x9c: case 0xac: case 0xbc: + s.template schedule(); + break; + case 0x8e: case 0x9e: case 0xae: case 0xbe: + s.template schedule(); + break; + case 0x9f: case 0xaf: case 0xbf: + s.template schedule(); + break; + case 0xce: case 0xde: case 0xee: case 0xfe: + s.template schedule(); + break; + case 0xdf: case 0xef: case 0xff: + s.template schedule(); + break; + } +} + +template <> +template void OperationMapper::dispatch(SchedulerT &s) { + using AM = AddressingMode; + using O = Operation; + + constexpr AddressingMode modes[] = { + AM::Immediate, AM::Direct, AM::Indexed, AM::Extended + }; + constexpr auto mode = modes[(i >> 4) & 3]; + + switch(i) { + default: s.template schedule(); break; + case 0x3f: s.template schedule(); break; + + case 0x83: case 0x93: case 0xa3: case 0xb3: + s.template schedule(); + break; + case 0x8c: case 0x9c: case 0xac: case 0xbc: + s.template schedule(); + break; + } +} + +} diff --git a/InstructionSets/AccessType.hpp b/InstructionSets/AccessType.hpp index 196bfc44b..37fdd9c1f 100644 --- a/InstructionSets/AccessType.hpp +++ b/InstructionSets/AccessType.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef AccessType_h -#define AccessType_h +#pragma once namespace InstructionSet { @@ -19,6 +18,3 @@ enum class AccessType { }; } - - -#endif /* AccessType_h */ diff --git a/InstructionSets/CachingExecutor.hpp b/InstructionSets/CachingExecutor.hpp index bc3204e2b..ac6f8b4e0 100644 --- a/InstructionSets/CachingExecutor.hpp +++ b/InstructionSets/CachingExecutor.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef CachingExecutor_hpp -#define CachingExecutor_hpp +#pragma once #include "../Numeric/Sizes.hpp" @@ -196,5 +195,3 @@ template < }; } - -#endif /* CachingExecutor_hpp */ diff --git a/InstructionSets/Disassembler.hpp b/InstructionSets/Disassembler.hpp index 71f88d59e..59cd9557f 100644 --- a/InstructionSets/Disassembler.hpp +++ b/InstructionSets/Disassembler.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef Disassembler_hpp -#define Disassembler_hpp +#pragma once #include "../Numeric/Sizes.hpp" @@ -85,5 +84,3 @@ template < }; } - -#endif /* Disassembler_h */ diff --git a/InstructionSets/M50740/Decoder.hpp b/InstructionSets/M50740/Decoder.hpp index 4f3f3d5b2..b744cd63e 100644 --- a/InstructionSets/M50740/Decoder.hpp +++ b/InstructionSets/M50740/Decoder.hpp @@ -6,16 +6,14 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef InstructionSets_M50740_Decoder_hpp -#define InstructionSets_M50740_Decoder_hpp +#pragma once #include "Instruction.hpp" #include #include -namespace InstructionSet { -namespace M50740 { +namespace InstructionSet::M50740 { class Decoder { public: @@ -34,6 +32,3 @@ class Decoder { }; } -} - -#endif /* InstructionSets_M50740_Decoder_hpp */ diff --git a/InstructionSets/M50740/Executor.cpp b/InstructionSets/M50740/Executor.cpp index 1bdf666c5..4c7a9cc22 100644 --- a/InstructionSets/M50740/Executor.cpp +++ b/InstructionSets/M50740/Executor.cpp @@ -8,19 +8,18 @@ #include "Executor.hpp" +#include "../../Machines/Utility/MemoryFuzzer.hpp" +#include "../../Outputs/Log.hpp" + #include #include #include -#include "../../Machines/Utility/MemoryFuzzer.hpp" - -#define LOG_PREFIX "[M50740] " -#include "../../Outputs/Log.hpp" - using namespace InstructionSet::M50740; namespace { constexpr int port_remap[] = {0, 1, 2, 0, 3}; + Log::Logger logger; } Executor::Executor(PortHandler &port_handler) : port_handler_(port_handler) { @@ -68,11 +67,9 @@ void Executor::set_interrupt_line(bool line) { // is active, amongst other things. if(interrupt_line_ != line) { interrupt_line_ = line; - - // TODO: verify interrupt connection. Externally, but stubbed off here. -// if(!interrupt_disable_ && line) { -// perform_interrupt(0x1ff4); -// } + if(line) { + set_interrupt_request(interrupt_control_, 0x02, 0x1ff4); + } } } @@ -85,13 +82,13 @@ uint8_t Executor::read(uint16_t address) { port_handler_.run_ports_for(cycles_since_port_handler_.flush()); switch(address) { default: - LOG("Unrecognised read from " << PADHEX(4) << address); + logger.error().append("Unrecognised read from %02x", address); return 0xff; // "Port R"; sixteen four-bit ports case 0xd0: case 0xd1: case 0xd2: case 0xd3: case 0xd4: case 0xd5: case 0xd6: case 0xd7: case 0xd8: case 0xd9: case 0xda: case 0xdb: case 0xdc: case 0xdd: case 0xde: case 0xdf: - LOG("Unimplemented Port R read from " << PADHEX(4) << address); + logger.error().append("Unimplemented Port R read from %04x", address); return 0x00; // Ports P0–P3. @@ -136,7 +133,7 @@ void Executor::write(uint16_t address, uint8_t value) { // ROM 'writes' are almost as easy (albeit unexpected). if(address >= 0x100) { - LOG("Attempted ROM write of " << PADHEX(2) << value << " to " << PADHEX(4) << address); + logger.info().append("Attempted ROM write of %02x to %04x", value, address); return; } @@ -145,13 +142,13 @@ void Executor::write(uint16_t address, uint8_t value) { switch(address) { default: - LOG("Unrecognised write of " << PADHEX(2) << value << " to " << PADHEX(4) << address); + logger.error().append("Unrecognised write of %02x to %04x", value, address); break; // "Port R"; sixteen four-bit ports case 0xd0: case 0xd1: case 0xd2: case 0xd3: case 0xd4: case 0xd5: case 0xd6: case 0xd7: case 0xd8: case 0xd9: case 0xda: case 0xdb: case 0xdc: case 0xdd: case 0xde: case 0xdf: - LOG("Unimplemented Port R write of " << PADHEX(2) << value << " from " << PADHEX(4) << address); + logger.error().append("Unimplemented Port R write of %02x to %04x", value, address); break; // Ports P0–P3. @@ -225,6 +222,16 @@ template inline void Executor::perform_interrupt(uint16_t vector) { set_program_counter(uint16_t(memory_[vector] | (memory_[vector+1] << 8))); } +void Executor::set_interrupt_request(uint8_t ®, uint8_t value, uint16_t vector) { + // TODO: this allows interrupts through only if fully enabled at the time they + // signal. Which isn't quite correct, albeit that it seems sufficient for the + // IIgs ADB controller. + reg |= value; + if(!interrupt_disable_ && (reg & (value >> 1))) { + perform_interrupt(vector); + } +} + template void Executor::perform() { // Post cycle cost; this emulation _does not provide accurate timing_. #define TLength(mode, base) case AddressingMode::mode: subtract_duration(base + t_lengths[index_mode_]); break; @@ -771,7 +778,7 @@ template void Executor::perform(uint8_t *operand [[maybe_u */ default: - LOG("Unimplemented operation: " << operation); + logger.error().append("Unimplemented operation: %d", operation); assert(false); } #undef set_nz @@ -789,12 +796,17 @@ inline void Executor::subtract_duration(int duration) { // this additional divide by 4 produces the correct net divide by 16. timer_divider_ += duration; - const int t12_ticks = update_timer(prescalers_[0], timer_divider_ / t12_divider); + const int clock_ticks = timer_divider_ / t12_divider; timer_divider_ &= (t12_divider-1); - // Update timers 1 and 2. TODO: interrupts (elsewhere?). - if(update_timer(timers_[0], t12_ticks)) interrupt_control_ |= 0x20; - if(update_timer(timers_[1], t12_ticks)) interrupt_control_ |= 0x08; + // Update timers 1 and 2. + const int t12_ticks = update_timer(prescalers_[0], timer_divider_ / t12_divider); + if(update_timer(timers_[0], t12_ticks)) { + set_interrupt_request(interrupt_control_, 0x20, 0x1ff8); + } + if(update_timer(timers_[1], t12_ticks)) { + set_interrupt_request(interrupt_control_, 0x08, 0x1ff6); + } // If timer X is disabled, stop. if(timer_control_&0x20) { @@ -804,18 +816,19 @@ inline void Executor::subtract_duration(int duration) { // Update timer X prescaler. switch(timer_control_ & 0x0c) { default: { - const int tx_ticks = update_timer(prescalers_[1], t12_ticks); // TODO: don't hard code this. And is this even right? - if(update_timer(timers_[2], tx_ticks)) - timer_control_ |= 0x80; // TODO: interrupt result of this. + const int tx_ticks = update_timer(prescalers_[1], clock_ticks); + if(update_timer(timers_[2], tx_ticks)) { + set_interrupt_request(timer_control_, 0x80, 0x1ffa); + } } break; case 0x04: - LOG("TODO: Timer X; Pulse output mode"); + logger.error().append("TODO: Timer X; Pulse output mode"); break; case 0x08: - LOG("TODO: Timer X; Event counter mode"); + logger.error().append("TODO: Timer X; Event counter mode"); break; case 0x0c: - LOG("TODO: Timer X; Pulse width measurement mode"); + logger.error().append("TODO: Timer X; Pulse width measurement mode"); break; } } diff --git a/InstructionSets/M50740/Executor.hpp b/InstructionSets/M50740/Executor.hpp index 28c83e43d..88d1edf00 100644 --- a/InstructionSets/M50740/Executor.hpp +++ b/InstructionSets/M50740/Executor.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef Executor_h -#define Executor_h +#pragma once #include "Instruction.hpp" #include "Parser.hpp" @@ -18,8 +17,7 @@ #include #include -namespace InstructionSet { -namespace M50740 { +namespace InstructionSet::M50740 { class Executor; using CachingExecutor = CachingExecutor; @@ -152,7 +150,7 @@ class Executor: public CachingExecutor { Timer timers_[3], prescalers_[2]; inline int update_timer(Timer &timer, int count); - // Interrupt and timer control. + // Interrupt and timer control. uint8_t interrupt_control_ = 0, timer_control_ = 0; bool interrupt_line_ = false; @@ -166,6 +164,8 @@ class Executor: public CachingExecutor { template inline void perform_interrupt(uint16_t vector); inline void set_port_output(int port); + void set_interrupt_request(uint8_t ®, uint8_t value, uint16_t vector); + // MARK: - Execution time Cycles cycles_; @@ -175,6 +175,3 @@ class Executor: public CachingExecutor { }; } -} - -#endif /* Executor_h */ diff --git a/InstructionSets/M50740/Instruction.hpp b/InstructionSets/M50740/Instruction.hpp index deafa692f..b61f2c17e 100644 --- a/InstructionSets/M50740/Instruction.hpp +++ b/InstructionSets/M50740/Instruction.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef InstructionSets_M50740_Instruction_h -#define InstructionSets_M50740_Instruction_h +#pragma once #include #include @@ -15,8 +14,7 @@ #include #include "../AccessType.hpp" -namespace InstructionSet { -namespace M50740 { +namespace InstructionSet::M50740 { enum class AddressingMode { Implied, Accumulator, Immediate, @@ -66,7 +64,7 @@ enum class Operation: uint8_t { INX, INY, DEX, DEY, FST, SLW, NOP, - PHA, PHP, PLA, PLP, + PHA, PHP, PLA, PLP, STP, TAX, TAY, TSX, TXA, TXS, TYA, @@ -121,7 +119,7 @@ inline constexpr const char *operation_name(Operation operation) { MAP(BCC); MAP(BCS); MAP(BEQ); MAP(BMI); MAP(BNE); MAP(BPL); MAP(BVC); MAP(BVS); MAP(BRA); MAP(BRK); MAP(JMP); MAP(JSR); MAP(RTI); MAP(RTS); MAP(CLC); MAP(CLD); MAP(CLI); MAP(CLT); MAP(CLV); MAP(SEC); MAP(SED); MAP(SEI); MAP(SET); MAP(INX); - MAP(INY); MAP(DEX); MAP(DEY); MAP(FST); MAP(SLW); MAP(NOP); MAP(PHA); MAP(PHP); + MAP(INY); MAP(DEX); MAP(DEY); MAP(FST); MAP(SLW); MAP(NOP); MAP(PHA); MAP(PHP); MAP(PLA); MAP(PLP); MAP(STP); MAP(TAX); MAP(TAY); MAP(TSX); MAP(TXA); MAP(TXS); MAP(TYA); MAP(ADC); MAP(SBC); MAP(AND); MAP(ORA); MAP(EOR); MAP(BIT); MAP(CMP); MAP(CPX); MAP(CPY); MAP(LDA); MAP(LDX); MAP(LDY); MAP(TST); MAP(ASL); MAP(LSR); @@ -186,7 +184,7 @@ inline std::string address(AddressingMode addressing_mode, const uint8_t *operat #define NUM(x) std::setfill('0') << std::setw(2) << int(x) #define NUM4(x) std::setfill('0') << std::setw(4) << int(x) switch(addressing_mode) { - default: return "???"; + default: return "???"; case AddressingMode::Implied: return ""; case AddressingMode::Accumulator: return "A "; case AddressingMode::Immediate: output << "#$" << NUM(operation[1]); break; @@ -202,10 +200,10 @@ inline std::string address(AddressingMode addressing_mode, const uint8_t *operat case AddressingMode::AbsoluteIndirect: output << "($" << NUM(operation[2]) << NUM(operation[1]) << ") "; break; case AddressingMode::ZeroPageIndirect: output << "($" << NUM(operation[1]) << ")"; break; case AddressingMode::SpecialPage: output << "$1f" << NUM(operation[1]); break; - case AddressingMode::ImmediateZeroPage: output << "#$" << NUM(operation[1]) << ", $" << NUM(operation[2]); break; - case AddressingMode::AccumulatorRelative: output << "A, $" << NUM4(2 + program_counter + int8_t(operation[1])); break; + case AddressingMode::ImmediateZeroPage: output << "#$" << NUM(operation[1]) << ", $" << NUM(operation[2]); break; + case AddressingMode::AccumulatorRelative: output << "A, $" << NUM4(2 + program_counter + int8_t(operation[1])); break; case AddressingMode::ZeroPageRelative: - output << "$" << NUM(operation[1]) << ", $" << NUM4(3 + program_counter + int8_t(operation[2])); + output << "$" << NUM(operation[1]) << ", $" << NUM4(3 + program_counter + int8_t(operation[2])); break; } #undef NUM4 @@ -236,7 +234,3 @@ inline std::ostream &operator <<(std::ostream &stream, const Instruction &instru } } -} - - -#endif /* InstructionSets_M50740_Instruction_h */ diff --git a/InstructionSets/M50740/Parser.hpp b/InstructionSets/M50740/Parser.hpp index 9c9937921..e9350b6eb 100644 --- a/InstructionSets/M50740/Parser.hpp +++ b/InstructionSets/M50740/Parser.hpp @@ -6,15 +6,13 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef InstructionSets_M50740_Parser_hpp -#define InstructionSets_M50740_Parser_hpp +#pragma once #include #include "Decoder.hpp" #include "../AccessType.hpp" -namespace InstructionSet { -namespace M50740 { +namespace InstructionSet::M50740 { template struct Parser { void parse(Target &target, const uint8_t *storage, uint16_t start, uint16_t closing_bound) { @@ -120,6 +118,3 @@ template struct Parser { }; } -} - -#endif /* InstructionSets_M50740_Parser_hpp */ diff --git a/InstructionSets/M68k/Decoder.cpp b/InstructionSets/M68k/Decoder.cpp index e63f29f6a..925804f5c 100644 --- a/InstructionSets/M68k/Decoder.cpp +++ b/InstructionSets/M68k/Decoder.cpp @@ -26,7 +26,7 @@ constexpr AddressingMode extended_modes[] = { }; /// @returns The @c AddressingMode given the specified mode and reg, subject to potential -/// aliasing on the '020+ as described above the @c AddressingMode enum. +/// aliasing on the '020+ as described above the @c AddressingMode enum. constexpr AddressingMode combined_mode(int mode, int reg) { assert(mode >= 0 && mode < 8); assert(reg >= 0 && reg < 8); @@ -71,7 +71,7 @@ uint32_t operand_mask(AddressingMode mode1, AddressingMode mode2) { /// immediate operand. template constexpr Operation Predecoder::operation(OpT op) { - if(op <= OpT(Operation::Max)) { + if(op <= OpMax) { return Operation(op); } @@ -140,7 +140,7 @@ constexpr Operation Predecoder::operation(OpT op) { } template -template uint32_t Predecoder::invalid_operands() { +template ::OpT op> uint32_t Predecoder::invalid_operands() { constexpr auto Dn = Mask< AddressingMode::DataRegisterDirect >::value; constexpr auto An = Mask< AddressingMode::AddressRegisterDirect >::value; constexpr auto Ind = Mask< AddressingMode::AddressRegisterIndirect >::value; @@ -154,6 +154,7 @@ template uint32_t Predecoder::invalid_operands() { constexpr auto d8PCXn = Mask< AddressingMode::ProgramCounterIndirectWithIndex8bitDisplacement >::value; constexpr auto Imm = Mask< AddressingMode::ImmediateData >::value; constexpr auto Quick = Mask< AddressingMode::Quick >::value; + constexpr auto Ext = Mask< AddressingMode::ExtensionWord >::value; // A few recurring combinations; terminology is directly from // the Programmers' Reference Manual. @@ -161,24 +162,27 @@ template uint32_t Predecoder::invalid_operands() { // // All modes: the complete set (other than Quick). // - static constexpr auto AllModes = Dn | An | Ind | PostInc | PreDec | d16An | d8AnXn | XXXw | XXXl | d16PC | d8PCXn | Imm; - static constexpr auto AllModesNoAn = AllModes & ~An; + static constexpr auto AllModes = Dn | An | Ind | PostInc | PreDec | d16An | d8AnXn | XXXw | XXXl | Imm | d16PC | d8PCXn; + static constexpr auto AllModesNoAn = AllModes & ~An; // // Alterable addressing modes (with and without AddressRegisterDirect). // - static constexpr auto AlterableAddressingModes = Dn | An | Ind | PostInc | PreDec | d16An | d8AnXn | XXXw | XXXl; + static constexpr auto AlterableAddressingModes = Dn | An | Ind | PostInc | PreDec | d16An | d8AnXn | XXXw | XXXl; static constexpr auto AlterableAddressingModesNoAn = AlterableAddressingModes & ~An; // // Control [flow] addressing modes. // - static constexpr auto ControlAddressingModes = Ind | d16An | d8AnXn | XXXw | XXXl | d16PC | d8PCXn; + static constexpr auto ControlAddressingModes = Ind | d16An | d8AnXn | XXXw | XXXl | d16PC | d8PCXn; + + // + // An invalid response, used as the default case. + // + static constexpr auto InvalidOperands = uint32_t(~0); switch(op) { - // By default, disallow all operands (including 'None'). This should catch any - // opcodes that are unmapped below. - default: return uint32_t(~0); + default: break; case OpT(Operation::ABCD): case OpT(Operation::ADDXb): case OpT(Operation::ADDXw): case OpT(Operation::ADDXl): @@ -191,11 +195,11 @@ template uint32_t Predecoder::invalid_operands() { case ADDtoRb: case ANDtoRb: case ANDtoRw: case ANDtoRl: - case OpT(Operation::CHK): + case OpT(Operation::CHKw): case OpT(Operation::CMPb): - case OpT(Operation::DIVU): case OpT(Operation::DIVS): + case OpT(Operation::DIVUw): case OpT(Operation::DIVSw): case ORtoRb: case ORtoRw: case ORtoRl: - case OpT(Operation::MULU): case OpT(Operation::MULS): + case OpT(Operation::MULUw): case OpT(Operation::MULSw): case SUBtoRb: return ~TwoOperandMask< AllModesNoAn, @@ -220,7 +224,7 @@ template uint32_t Predecoder::invalid_operands() { >::value; case OpT(Operation::ADDAw): case OpT(Operation::ADDAl): - case OpT(Operation::CMPAw): case OpT(Operation::CMPAl): + case OpT(Operation::CMPAw): case OpT(Operation::CMPAl): case OpT(Operation::SUBAw): case OpT(Operation::SUBAl): case OpT(Operation::MOVEAw): case OpT(Operation::MOVEAl): return ~TwoOperandMask< @@ -279,8 +283,7 @@ template uint32_t Predecoder::invalid_operands() { >::value; case OpT(Operation::ANDItoCCR): case OpT(Operation::ANDItoSR): - case OpT(Operation::Bccw): case OpT(Operation::Bccl): - case OpT(Operation::BSRl): case OpT(Operation::BSRw): + case OpT(Operation::Bccw): case OpT(Operation::BSRw): case OpT(Operation::EORItoCCR): case OpT(Operation::EORItoSR): case OpT(Operation::ORItoCCR): case OpT(Operation::ORItoSR): case OpT(Operation::STOP): @@ -362,7 +365,7 @@ template uint32_t Predecoder::invalid_operands() { >::value; case OpT(Operation::TSTb): - if constexpr (model == Model::M68000) { + if constexpr (model < Model::M68020) { return ~OneOperandMask< AlterableAddressingModesNoAn >::value; @@ -375,7 +378,7 @@ template uint32_t Predecoder::invalid_operands() { >::value; case OpT(Operation::TSTw): case OpT(Operation::TSTl): - if constexpr (model == Model::M68000) { + if constexpr (model < Model::M68020) { return ~OneOperandMask< AlterableAddressingModesNoAn >::value; @@ -457,13 +460,13 @@ template uint32_t Predecoder::invalid_operands() { case OpT(Operation::MOVEMtoMw): case OpT(Operation::MOVEMtoMl): return ~TwoOperandMask< - Imm, + Ext, Ind | PreDec | d16An | d8AnXn | XXXw | XXXl >::value; case OpT(Operation::MOVEMtoRw): case OpT(Operation::MOVEMtoRl): return ~TwoOperandMask< - Imm, + Ext, Ind | PostInc | d16An | d8AnXn | XXXw | XXXl | d16PC | d8PCXn >::value; @@ -479,14 +482,149 @@ template uint32_t Predecoder::invalid_operands() { d16An >::value; } + + + // + // 68010 additions. + // + if constexpr (model < Model::M68010) { + return InvalidOperands; + } + switch(op) { + default: break; + + case OpT(Operation::BKPT): + return ~OneOperandMask< + Quick + >::value; + + case OpT(Operation::RTD): + return ~OneOperandMask< + Imm + >::value; + + case OpT(Operation::MOVEfromCCR): + return ~OneOperandMask< + AlterableAddressingModesNoAn + >::value; + + case OpT(Operation::MOVEfromC): case OpT(Operation::MOVEtoC): + return ~OneOperandMask< + Ext + >::value; + + case OpT(Operation::MOVESb): case OpT(Operation::MOVESw): case OpT(Operation::MOVESl): + return ~TwoOperandMask< + Ext, + Ind | PostInc | PreDec | d16An | d8AnXn | XXXw | XXXl + >::value; + } + + // + // 68020 additions. + // + if constexpr (model < Model::M68020) { + return InvalidOperands; + } + switch(op) { + default: break; + + case OpT(Operation::TRAPcc): + return ~TwoOperandMask< + Ext | NoOperand, + Ext | NoOperand + >::value; + + case OpT(Operation::Bccl): case OpT(Operation::BSRl): + return ~OneOperandMask< + Imm + >::value; + + case OpT(Operation::RTM): + return ~OneOperandMask< + An | Dn + >::value; + + case OpT(Operation::CALLM): + return ~OneOperandMask< + ControlAddressingModes + >::value; + + case OpT(Operation::BFCHG): case OpT(Operation::BFCLR): case OpT(Operation::BFSET): + case OpT(Operation::BFINS): + return ~TwoOperandMask< + Ext, + Dn | Ind | d16An | d8AnXn | XXXw | XXXl + >::value; + + case OpT(Operation::BFTST): case OpT(Operation::BFFFO): case OpT(Operation::BFEXTU): + case OpT(Operation::BFEXTS): + return ~TwoOperandMask< + Ext, + Dn | Ind | d16An | d8AnXn | XXXw | XXXl | d16PC | d8PCXn + >::value; + + case OpT(Operation::PACK): case OpT(Operation::UNPK): + return ~TwoOperandMask< + Dn | PreDec, + Dn | PreDec + >::value; + + case OpT(Operation::CASb): case OpT(Operation::CASw): case OpT(Operation::CASl): + return ~TwoOperandMask< + Ext, + Ind | PostInc | PreDec | d16An | d8AnXn | XXXw | XXXl + >::value; + + case OpT(Operation::CAS2w): case OpT(Operation::CAS2l): + return ~TwoOperandMask< + Ext, + Ext + >::value; + + case OpT(Operation::CHKorCMP2b): + case OpT(Operation::CHKorCMP2w): + case OpT(Operation::CHKorCMP2l): + return ~TwoOperandMask< + Ext, + ControlAddressingModes + >::value; + + case OpT(Operation::LINKl): + return ~TwoOperandMask< + An, + Imm + >::value; + + case OpT(Operation::CHKl): + return ~TwoOperandMask< + AllModesNoAn, + Dn + >::value; + + case OpT(Operation::EXTbtol): + return ~OneOperandMask< + Dn + >::value; + + case OpT(Operation::DIVSorDIVUl): + case OpT(Operation::MULSorMULUl): + return ~TwoOperandMask< + Ext, + AllModesNoAn + >::value; + } + + return InvalidOperands; } /// Provides a post-decoding validation step — primarily ensures that the prima facie addressing modes are supported by the operation. template -template Preinstruction Predecoder::validated( +template ::OpT op, bool validate> Preinstruction Predecoder::validated( AddressingMode op1_mode, int op1_reg, AddressingMode op2_mode, int op2_reg, - Condition condition + Condition condition, + int additional_extension_words ) { constexpr auto operation = Predecoder::operation(op); @@ -496,6 +634,7 @@ template Preinstruction Predecoder::validated op1_mode, op1_reg, op2_mode, op2_reg, requires_supervisor(operation), + additional_extension_words, operand_size(operation), condition); } @@ -509,6 +648,7 @@ template Preinstruction Predecoder::validated op1_mode, op1_reg, op2_mode, op2_reg, requires_supervisor(operation), + additional_extension_words, operand_size(operation), condition); } @@ -516,7 +656,7 @@ template Preinstruction Predecoder::validated /// Decodes the fields within an instruction and constructs a `Preinstruction`, given that the operation has already been /// decoded. Optionally applies validation template -template Preinstruction Predecoder::decode(uint16_t instruction) { +template ::OpT op, bool validate> Preinstruction Predecoder::decode(uint16_t instruction) { // Fields used pervasively below. // // Underlying assumption: the compiler will discard whatever of these @@ -526,7 +666,11 @@ template Preinstruction Predecoder::decode(ui const auto opmode = (instruction >> 6) & 7; const auto data_register = (instruction >> 9) & 7; + // + // Operations common to all processors. + // switch(op) { + default: break; // // MARK: ABCD, SBCD, ADDX. @@ -584,7 +728,7 @@ template Preinstruction Predecoder::decode(ui // Implicitly: source is an immediate value; // b0–b2 and b3–b5: destination effective address. // - case EORIb: case EORIl: case EORIw: + case EORIb: case EORIl: case EORIw: case ORIb: case ORIl: case ORIw: case ANDIb: case ANDIl: case ANDIw: case SUBIb: case SUBIl: case SUBIw: @@ -610,42 +754,42 @@ template Preinstruction Predecoder::decode(ui combined_mode(ea_mode, ea_register), ea_register); // - // MARK: STOP, ANDItoCCR, ANDItoSR, EORItoCCR, EORItoSR, ORItoCCR, ORItoSR, BSRl, BSRw + // MARK: STOP, ANDItoCCR, ANDItoSR, EORItoCCR, EORItoSR, ORItoCCR, ORItoSR, BSRw // // Operand is an immedate; destination/source (if any) is implied by the operation, // e.g. ORItoSR has a destination of SR. // case OpT(Operation::STOP): - case OpT(Operation::BSRl): case OpT(Operation::BSRw): + case OpT(Operation::BSRw): case OpT(Operation::ORItoSR): case OpT(Operation::ORItoCCR): case OpT(Operation::ANDItoSR): case OpT(Operation::ANDItoCCR): case OpT(Operation::EORItoSR): case OpT(Operation::EORItoCCR): return validated(AddressingMode::ImmediateData); // - // MARK: Bccl, Bccw + // MARK: Bccw // // Operand is an immedate; b8–b11 are a condition code. // - case OpT(Operation::Bccl): case OpT(Operation::Bccw): + case OpT(Operation::Bccw): return validated( AddressingMode::ImmediateData, 0, AddressingMode::None, 0, Condition((instruction >> 8) & 0xf)); // - // MARK: CHK + // MARK: CHKw // // Implicitly: destination is a register; // b0–b2 and b3–b5: source effective address. // - case OpT(Operation::CHK): + case OpT(Operation::CHKw): return validated( combined_mode(ea_mode, ea_register), ea_register, AddressingMode::DataRegisterDirect, data_register); // - // MARK: EXG. + // MARK: EXG // // b0–b2: register Ry (data or address, address if exchange is address <-> data); // b9–b11: register Rx (data or address, data if exchange is address <-> data); @@ -672,8 +816,8 @@ template Preinstruction Predecoder::decode(ui // b9–b11: destination data register; // b0–b2 and b3–b5: source effective address. // - case OpT(Operation::DIVU): case OpT(Operation::DIVS): - case OpT(Operation::MULU): case OpT(Operation::MULS): + case OpT(Operation::DIVUw): case OpT(Operation::DIVSw): + case OpT(Operation::MULUw): case OpT(Operation::MULSw): return validated( combined_mode(ea_mode, ea_register), ea_register, AddressingMode::DataRegisterDirect, data_register); @@ -798,13 +942,13 @@ template Preinstruction Predecoder::decode(ui case OpT(Operation::MOVEMtoMl): case OpT(Operation::MOVEMtoMw): case OpT(Operation::MOVEMtoRl): case OpT(Operation::MOVEMtoRw): return validated( - AddressingMode::ImmediateData, 0, + AddressingMode::ExtensionWord, 0, combined_mode(ea_mode, ea_register), ea_register); // // MARK: TRAP, BCCb, BSRb // - // No further operands decoded, but note that one is somewhere in the opcode. + // No further operands decoded, but one is somewhere in the opcode. // case OpT(Operation::TRAP): case OpT(Operation::BSRb): @@ -858,6 +1002,7 @@ template Preinstruction Predecoder::decode(ui // b0–b2: a register to shift (the source here, for consistency with the memory operations); // b8: 0 => b9–b11 are a direct count of bits to shift; 1 => b9–b11 identify a register containing the shift count; // b9–b11: either a quick value or a register. + // case OpT(Operation::ASRb): case OpT(Operation::ASRw): case OpT(Operation::ASRl): case OpT(Operation::LSRb): case OpT(Operation::LSRw): case OpT(Operation::LSRl): case OpT(Operation::ROXRb): case OpT(Operation::ROXRw): case OpT(Operation::ROXRl): @@ -885,19 +1030,247 @@ template Preinstruction Predecoder::decode(ui return validated( AddressingMode::AddressRegisterIndirectWithPostincrement, ea_register, AddressingMode::AddressRegisterIndirectWithPostincrement, data_register); + } + + // + // 68010 additions. + // + if constexpr (model < Model::M68010) { + assert(false); + } + switch(op) { + default: break; // - // MARK: Impossible error case. + // MARK: BKPT // - default: - // Should be unreachable. - assert(false); + // No further operands decoded, but one is somewhere in the opcode. + // + case OpT(Operation::BKPT): + return validated(AddressingMode::Quick); + + // + // MARK: RTD + // + case OpT(Operation::RTD): + return validated(AddressingMode::ImmediateData); + + // + // MARK: MOVE from CCR. + // + case OpT(Operation::MOVEfromCCR): + return validated(combined_mode(ea_mode, ea_register), ea_register); + + // + // MARK: MOVE to/from C. + // + // No further information in the instruction, but an extension word is required. + // + case OpT(Operation::MOVEfromC): case OpT(Operation::MOVEtoC): + return validated(AddressingMode::ExtensionWord); + + // + // MARK: MOVES + // + // b0–b2 and b3–b5: effective address; + // also an extension word is present to dictate a further register and a direction of transfer. + // + case OpT(Operation::MOVESb): case OpT(Operation::MOVESw): case OpT(Operation::MOVESl): + return validated( + AddressingMode::ExtensionWord, 0, + combined_mode(ea_mode, ea_register), ea_register); } + + // + // 68020 additions. + // + if constexpr (model < Model::M68020) { + assert(false); + } + switch(op) { + default: break; + + // + // MARK: BSRl + // + // Operand is an immedate. + // + case OpT(Operation::BSRl): + return validated(AddressingMode::ImmediateData); + + // + // MARK: Bccl + // + // Operand is an immedate; b8–b11 are a condition code. + // + case OpT(Operation::Bccl): + return validated( + AddressingMode::ImmediateData, 0, + AddressingMode::None, 0, + Condition((instruction >> 8) & 0xf)); + + // + // MARK: DIVSl. + // + // b0–b2 and b3–b5: source effective address. + // Plus an immediate word operand + // + case OpT(Operation::DIVSorDIVUl): + case OpT(Operation::MULSorMULUl): + return validated( + AddressingMode::ExtensionWord, 0, + combined_mode(ea_mode, ea_register), ea_register); + + // + // MARK: BFCHG, BFTST, BFFFO, BFEXTU, BFEXTS, BFCLR, BFSET, BFINS + // + // b0–b2 and b3–b5: an effective address. + // There is also an immedate operand describing an offset and width. + // + case OpT(Operation::BFCHG): case OpT(Operation::BFTST): case OpT(Operation::BFFFO): + case OpT(Operation::BFEXTU): case OpT(Operation::BFEXTS): case OpT(Operation::BFCLR): + case OpT(Operation::BFSET): case OpT(Operation::BFINS): + return validated( + AddressingMode::ExtensionWord, 0, + combined_mode(ea_mode, ea_register), ea_register); + + // + // MARK: CALLM + // + // b0–b2 and b3–b5: an effective address. + // There is also an immedate operand providing argument count. + // + case OpT(Operation::CALLM): + return validated( + AddressingMode::ImmediateData, 0, + combined_mode(ea_mode, ea_register), ea_register); + + // + // MARK: RTM + // + // b0–b2: a register number; + // b3: address/data register selection. + // + case OpT(Operation::RTM): { + const auto addressing_mode = (instruction & 8) ? + AddressingMode::AddressRegisterDirect : AddressingMode::DataRegisterDirect; + + return validated(addressing_mode, ea_register); + } + + // + // MARK: TRAPcc + // + // Has 0, 1 or 2 following words, neither of which contributes to operation. + // + case OpT(Operation::TRAPcc): { + switch(instruction & 7) { + default: return Preinstruction(); + + // No extension. + case 4: return validated(); + + // Word-sized extension. + case 2: return validated(AddressingMode::ExtensionWord); + + // DWord-sized extension (which is encoded as two extension operands). + case 3: + return validated( + AddressingMode::ExtensionWord, 0, + AddressingMode::ExtensionWord, 0); + } + } + + // + // MARK: PACK, UNPK + // + // b9–b11: Rx (destination) + // b0–b2: Ry (source) + // b3: 1 => operation is memory-to-memory; 0 => register-to-register. + // This instruction is also followed by a 16-bit adjustment extension. + // + case OpT(Operation::PACK): + case OpT(Operation::UNPK): { + const auto addressing_mode = (instruction & 8) ? + AddressingMode::AddressRegisterIndirectWithPredecrement : AddressingMode::DataRegisterDirect; + + return validated( + addressing_mode, ea_register, + addressing_mode, data_register, + Condition::True, 1); + } + + // + // MARK: CAS + // + // b0–b2 and b3–b5: an effective address. + // There is also an immedate operand describing relevant registers. + // + case OpT(Operation::CASb): case OpT(Operation::CASw): case OpT(Operation::CASl): + case OpT(Operation::CHKorCMP2b): + case OpT(Operation::CHKorCMP2w): + case OpT(Operation::CHKorCMP2l): + return validated( + AddressingMode::ExtensionWord, 0, + combined_mode(ea_mode, ea_register), ea_register); + + // + // MARK: CAS2 + // + // b0–b2 and b3–b5: an effective address. + // There is also an immedate operand describing relevant registers. + // + case OpT(Operation::CAS2w): case OpT(Operation::CAS2l): + return validated( + AddressingMode::ExtensionWord, 0, + AddressingMode::ExtensionWord, 0); + + // + // MARK: LINKl + // + // b0–b2: 'source' address register; + // Implicitly: 'destination' is an immediate. + // + case OpT(Operation::LINKl): + return validated( + AddressingMode::AddressRegisterDirect, ea_register, + AddressingMode::ImmediateData, 0); + + // + // MARK: CHKl + // + // Implicitly: destination is a register; + // b0–b2 and b3–b5: source effective address. + // + case OpT(Operation::CHKl): + return validated( + combined_mode(ea_mode, ea_register), ea_register, + AddressingMode::DataRegisterDirect, data_register); + + // + // MARK: EXTbtol + // + // b0–b2: a data register. + // + case OpT(Operation::EXTbtol): + return validated(AddressingMode::DataRegisterDirect, ea_register); + + // + // MARK: DIVl + // + // + // TODO. + } + + // Should be unreachable. + assert(false); + return Preinstruction(); // To appease GCC during development. } // MARK: - Page decoders. #define Decode(y) return decode(instruction) +#define DecodeReq(x, y) if constexpr (x) Decode(y); break; template Preinstruction Predecoder::decode0(uint16_t instruction) { @@ -911,6 +1284,16 @@ Preinstruction Predecoder::decode0(uint16_t instruction) { case 0xa3c: Decode(Op::EORItoCCR); // 4-104 (p208) case 0xa7c: Decode(Op::EORItoSR); // 6-10 (p464) + // 4-68 (p172) + case 0xcfc: DecodeReq(model >= Model::M68020, Op::CAS2w); + case 0xefc: DecodeReq(model >= Model::M68020, Op::CAS2l); + + default: break; + } + + switch(instruction & 0xff0) { + case 0x6c0: DecodeReq(model == Model::M68020, Op::RTM); // 4-167 (p271) + default: break; } @@ -957,6 +1340,25 @@ Preinstruction Predecoder::decode0(uint16_t instruction) { case 0xc40: Decode(CMPIw); case 0xc80: Decode(CMPIl); + // 4-64 (p168) + case 0x6c0: DecodeReq(model == Model::M68020, Op::CALLM); + + // 4-67 (p171) + case 0xac0: DecodeReq(model >= Model::M68020, Op::CASb); + case 0xcc0: DecodeReq(model >= Model::M68020, Op::CASw); + case 0xec0: DecodeReq(model >= Model::M68020, Op::CASl); + + // 4-83 (p187) [CMP2] and 4-72 (p176) [CHK2]; + // the two are distinguished by a bit in the extension word. + case 0x0c0: DecodeReq(model >= Model::M68020, Op::CHKorCMP2b); + case 0x2c0: DecodeReq(model >= Model::M68020, Op::CHKorCMP2b); + case 0x4c0: DecodeReq(model >= Model::M68020, Op::CHKorCMP2b); + + // 6-24 (p478) + case 0xe00: DecodeReq(model >= Model::M68010, Op::MOVESb); + case 0xe40: DecodeReq(model >= Model::M68010, Op::MOVESw); + case 0xe80: DecodeReq(model >= Model::M68010, Op::MOVESl); + default: break; } @@ -1011,7 +1413,6 @@ Preinstruction Predecoder::decode3(uint16_t instruction) { case 0x040: Decode(Op::MOVEAw); default: Decode(Op::MOVEw); } -// Decode(Op::MOVEw); } template @@ -1023,20 +1424,31 @@ Preinstruction Predecoder::decode4(uint16_t instruction) { case 0xe71: Decode(Op::NOP); // 4-147 (p251) case 0xe72: Decode(Op::STOP); // 6-85 (p539) case 0xe73: Decode(Op::RTE); // 6-84 (p538) + case 0xe74: DecodeReq(model >= Model::M68010, Op::RTD); // 4-166 (p270) case 0xe75: Decode(Op::RTS); // 4-169 (p273) case 0xe76: Decode(Op::TRAPV); // 4-191 (p295) case 0xe77: Decode(Op::RTR); // 4-168 (p272) + case 0xe7a: DecodeReq(model >= Model::M68010, Op::MOVEtoC); // 6-22 (p476) + case 0xe7b: DecodeReq(model >= Model::M68010, Op::MOVEfromC); // 6-22 (p476) default: break; } switch(instruction & 0xff8) { case 0x840: Decode(Op::SWAP); // 4-185 (p289) - case 0x880: Decode(Op::EXTbtow); // 4-106 (p210) - case 0x8c0: Decode(Op::EXTwtol); // 4-106 (p210) - case 0xe50: Decode(Op::LINKw); // 4-111 (p215) + case 0x848: DecodeReq(model >= Model::M68010, Op::BKPT); // 4-54 (p158) case 0xe58: Decode(Op::UNLINK); // 4-194 (p298) case 0xe60: Decode(Op::MOVEtoUSP); // 6-21 (p475) case 0xe68: Decode(Op::MOVEfromUSP); // 6-21 (p475) + + // 4-106 (p210) + case 0x880: Decode(Op::EXTbtow); + case 0x8c0: Decode(Op::EXTwtol); + case 0x9c0: DecodeReq(model >= Model::M68020, Op::EXTbtol); + + // 4-111 (p215) + case 0x808: DecodeReq(model >= Model::M68020, Op::LINKl); + case 0xe50: Decode(Op::LINKw); + default: break; } @@ -1101,12 +1513,25 @@ Preinstruction Predecoder::decode4(uint16_t instruction) { // 4-108 (p212) case 0xec0: Decode(Op::JMP); + // 4-94 (p198) [DIVS.l]; 4-98 (p202) [DIVU.l] + case 0xc40: DecodeReq(model >= Model::M68020, Op::DIVSorDIVUl); + + // 4-137 (p241) [MULS.l]; 4-140 (p244) [MULU.l] + case 0xc00: DecodeReq(model >= Model::M68020, Op::MULSorMULUl); + + // 4-121 (p225) + case 0x2c0: DecodeReq(model >= Model::M68010, Op::MOVEfromCCR); + default: break; } switch(instruction & 0x1c0) { case 0x1c0: Decode(Op::LEA); // 4-110 (p214) - case 0x180: Decode(Op::CHK); // 4-69 (p173) + + // 4-69 (p173) + case 0x180: Decode(Op::CHKw); + case 0x100: DecodeReq(model >= Model::M68020, Op::CHKl); + default: break; } @@ -1165,12 +1590,23 @@ Preinstruction Predecoder::decode5(uint16_t instruction) { default: break; } + switch(instruction & 0x0ff) { + // 4-173 (p276) + case 0x0f8: case 0x0f9: Decode(Op::Scc); + + // 4-189 (p294) + case 0x0fa: case 0x0fb: case 0x0fc: + DecodeReq(model >= Model::M68020, Op::TRAPcc); + + default: break; + } + switch(instruction & 0x0f8) { // 4-173 (p276) case 0x0c0: case 0x0d0: case 0x0d8: case 0x0e0: case 0x0e8: - case 0x0f0: case 0x0f8: Decode(Op::Scc); + case 0x0f0: Decode(Op::Scc); // 4-91 (p195) case 0x0c8: Decode(Op::DBcc); @@ -1226,12 +1662,17 @@ template Preinstruction Predecoder::decode8(uint16_t instruction) { using Op = Operation; - // 4-171 (p275) - if((instruction & 0x1f0) == 0x100) Decode(Op::SBCD); + switch(instruction & 0x1f0) { + case 0x100: Decode(Op::SBCD); // 4-171 (p275) + case 0x140: DecodeReq(model >= Model::M68020, Op::PACK); // 4-157 (p261) + case 0x180: DecodeReq(model >= Model::M68020, Op::UNPK); // 4-196 (p300) + + default: break; + } switch(instruction & 0x1c0) { - case 0x0c0: Decode(Op::DIVU); // 4-97 (p201) - case 0x1c0: Decode(Op::DIVS); // 4-93 (p197) + case 0x0c0: Decode(Op::DIVUw); // 4-97 (p201) + case 0x1c0: Decode(Op::DIVSw); // 4-93 (p197) // 4-150 (p254) case 0x000: Decode(ORtoRb); @@ -1336,8 +1777,8 @@ Preinstruction Predecoder::decodeC(uint16_t instruction) { } switch(instruction & 0x1c0) { - case 0x0c0: Decode(Op::MULU); // 4-139 (p243) - case 0x1c0: Decode(Op::MULS); // 4-136 (p240) + case 0x0c0: Decode(Op::MULUw); // 4-139 (p243) + case 0x1c0: Decode(Op::MULSw); // 4-136 (p240) // 4-15 (p119) case 0x000: Decode(ANDtoRb); @@ -1399,6 +1840,15 @@ Preinstruction Predecoder::decodeE(uint16_t instruction) { case 0x6c0: Decode(Op::RORm); // 4-160 (p264) case 0x7c0: Decode(Op::ROLm); // 4-160 (p264) + case 0x8c0: DecodeReq(model >= Model::M68020, Op::BFTST); // 4-51 (p155) + case 0x9c0: DecodeReq(model >= Model::M68020, Op::BFEXTU); // 4-40 (p144) + case 0xac0: DecodeReq(model >= Model::M68020, Op::BFCHG); // 4-33 (p137) + case 0xbc0: DecodeReq(model >= Model::M68020, Op::BFEXTS); // 4-37 (p141) + case 0xcc0: DecodeReq(model >= Model::M68020, Op::BFCLR); // 4-35 (p139) + case 0xdc0: DecodeReq(model >= Model::M68020, Op::BFFFO); // 4-43 (p147) [though the given opcode is wrong; listed same as BFEXTU] + case 0xec0: DecodeReq(model >= Model::M68020, Op::BFSET); // 4-49 (p153) + case 0xfc0: DecodeReq(model >= Model::M68020, Op::BFINS); // 4-46 (p150) + default: break; } @@ -1455,6 +1905,7 @@ Preinstruction Predecoder::decodeF(uint16_t) { } #undef Decode +#undef DecodeRef // MARK: - Main decoder. diff --git a/InstructionSets/M68k/Decoder.hpp b/InstructionSets/M68k/Decoder.hpp index 30052304a..5282b8181 100644 --- a/InstructionSets/M68k/Decoder.hpp +++ b/InstructionSets/M68k/Decoder.hpp @@ -6,14 +6,13 @@ // Copyright © 2022 Thomas Harte. All rights reserved. // -#ifndef InstructionSets_M68k_Decoder_hpp -#define InstructionSets_M68k_Decoder_hpp +#pragma once #include "Instruction.hpp" #include "Model.hpp" +#include "../../Numeric/Sizes.hpp" -namespace InstructionSet { -namespace M68k { +namespace InstructionSet::M68k { /*! A stateless decoder that can map from instruction words to preinstructions @@ -50,23 +49,32 @@ template class Predecoder { Preinstruction decodeE(uint16_t instruction); Preinstruction decodeF(uint16_t instruction); - using OpT = uint8_t; + // Yuckiness here: 67 is a count of the number of things contained below in + // ExtendedOperation; this acts to ensure ExtendedOperation is the minimum + // integer size large enough to hold all actual operations plus the ephemeral + // ones used here. Intention is to support table-based decoding, which will mean + // making those integers less ephemeral, hence the desire to pick a minimum size. + using OpT = typename MinIntTypeValue< + uint64_t(OperationMax::value) + 67 + >::type; + static constexpr auto OpMax = OpT(OperationMax::value); // Specific instruction decoders. template Preinstruction decode(uint16_t instruction); template Preinstruction validated( AddressingMode op1_mode = AddressingMode::None, int op1_reg = 0, AddressingMode op2_mode = AddressingMode::None, int op2_reg = 0, - Condition condition = Condition::True + Condition condition = Condition::True, + int further_extension_words = 0 ); - template uint32_t invalid_operands(); + template uint32_t invalid_operands(); // Extended operation list; collapses into a single byte enough information to // know both the type of operation and how to decode the operands. Most of the // time that's knowable from the Operation alone, hence the rather awkward // extension of @c Operation. enum ExtendedOperation: OpT { - MOVEPtoRl = uint8_t(Operation::Max) + 1, MOVEPtoRw, + MOVEPtoRl = OpMax + 1, MOVEPtoRw, MOVEPtoMl, MOVEPtoMw, MOVEQ, @@ -87,8 +95,6 @@ template class Predecoder { CMPMb, CMPMw, CMPMl, - MOVEq, - ADDtoMb, ADDtoMw, ADDtoMl, ADDtoRb, ADDtoRw, ADDtoRl, @@ -108,6 +114,3 @@ template class Predecoder { }; } -} - -#endif /* InstructionSets_M68k_Decoder_hpp */ diff --git a/InstructionSets/M68k/ExceptionVectors.hpp b/InstructionSets/M68k/ExceptionVectors.hpp index e56c71efb..1acf79b52 100644 --- a/InstructionSets/M68k/ExceptionVectors.hpp +++ b/InstructionSets/M68k/ExceptionVectors.hpp @@ -6,11 +6,9 @@ // Copyright © 2022 Thomas Harte. All rights reserved. // -#ifndef InstructionSets_M68k_ExceptionVectors_hpp -#define InstructionSets_M68k_ExceptionVectors_hpp +#pragma once -namespace InstructionSet { -namespace M68k { +namespace InstructionSet::M68k { enum Exception { InitialStackPointer = 0, @@ -45,6 +43,3 @@ enum Exception { }; } -} - -#endif /* InstructionSets_M68k_ExceptionVectors_hpp */ diff --git a/InstructionSets/M68k/Executor.hpp b/InstructionSets/M68k/Executor.hpp index 0fe9ef3a6..7d0f58169 100644 --- a/InstructionSets/M68k/Executor.hpp +++ b/InstructionSets/M68k/Executor.hpp @@ -6,8 +6,7 @@ // Copyright © 2022 Thomas Harte. All rights reserved. // -#ifndef InstructionSets_M68k_Executor_hpp -#define InstructionSets_M68k_Executor_hpp +#pragma once #include "Decoder.hpp" #include "Instruction.hpp" @@ -16,13 +15,12 @@ #include "RegisterSet.hpp" #include "Status.hpp" -namespace InstructionSet { -namespace M68k { +namespace InstructionSet::M68k { /// Maps the 68k function codes such that bits 0, 1 and 2 represent /// FC0, FC1 and FC2 respectively. enum class FunctionCode { - UserData = 0b001, + UserData = 0b001, UserProgram = 0b010, SupervisorData = 0b101, SupervisorProgram = 0b110, @@ -159,13 +157,10 @@ template class Executor { bool requires_fetch; }; EffectiveAddress calculate_effective_address(Preinstruction instruction, uint16_t opcode, int index); - uint32_t index_8bitdisplacement(); + uint32_t index_8bitdisplacement(uint32_t); } state_; }; -} } #include "Implementation/ExecutorImplementation.hpp" - -#endif /* InstructionSets_M68k_Executor_hpp */ diff --git a/InstructionSets/M68k/Implementation/ExecutorImplementation.hpp b/InstructionSets/M68k/Implementation/ExecutorImplementation.hpp index 1d129a354..ef65f232c 100644 --- a/InstructionSets/M68k/Implementation/ExecutorImplementation.hpp +++ b/InstructionSets/M68k/Implementation/ExecutorImplementation.hpp @@ -1,4 +1,5 @@ // +// // ExecutorImplementation.hpp // Clock Signal // @@ -6,16 +7,14 @@ // Copyright © 2022 Thomas Harte. All rights reserved. // -#ifndef InstructionSets_M68k_ExecutorImplementation_hpp -#define InstructionSets_M68k_ExecutorImplementation_hpp +#pragma once #include "../Perform.hpp" #include "../ExceptionVectors.hpp" #include -namespace InstructionSet { -namespace M68k { +namespace InstructionSet::M68k { #define An(x) state_.registers[8 + x] #define Dn(x) state_.registers[x] @@ -199,16 +198,70 @@ template IntT Executor::State::read_pc() { return result; } +// For all of below, cf PRM 2-2 (PDF p43) template -uint32_t Executor::State::index_8bitdisplacement() { - // TODO: if not a 68000, check bit 8 for whether this should be a full extension word; - // also include the scale field even if not. +uint32_t Executor::State::index_8bitdisplacement(uint32_t base) { + // Determine whether full extension addressing modes are supported. + constexpr bool supports_full_extensions = model >= Model::M68020; + + // Get the [first] extension word. const auto extension = read_pc(); + + // The 68000, 68080 and 68010 do not support the scale field, and are limited + // to brief extension words. + const int scale = supports_full_extensions ? (extension >> 9) & 3 : 0; + + // Decode brief instruction word fields. const auto offset = int8_t(extension); const int register_index = (extension >> 12) & 15; - const uint32_t displacement = registers[register_index].l; - const uint32_t sized_displacement = (extension & 0x800) ? displacement : int16_t(displacement); - return offset + sized_displacement; + + // Calculate the displacement; which on the 68020+ is better known as the index. + const uint32_t raw_index = registers[register_index].l; + uint32_t index = ((extension & 0x800) ? raw_index : int16_t(raw_index)) << scale; + + // Use a brief extension word if instructed to, or if that's this processor's limit. + if(!supports_full_extensions || !(extension & 0x100)) { + return base + offset + index; + } + + // + // Determine a long extension. + // + + // Apply suppressions. + const bool suppress_base = extension & 0x80; // i.e. don't use whatever the first instruction word indicated. + const bool suppress_index = extension & 0x40; // i.e. don't use whatever register_index points to. + if(suppress_base) base = 0; + if(suppress_index) index = 0; + + // Fetch base displacement. + uint32_t base_displacement = 0; + switch((extension >> 4) & 3) { + default: break; + case 2: base_displacement = read_pc(); break; + case 3: base_displacement = read_pc(); break; + } + + // Don't do a further indirection if there's no outer displacement. + if(!(extension & 7)) { + return index + base + base_displacement; + } + + // Fetch outer displacement. + uint32_t outer_displacement = 0; + switch(extension & 3) { + default: break; + case 2: outer_displacement = read_pc(); break; + case 3: outer_displacement = read_pc(); break; + } + + // Apply outer displacement; either the index is before the indirection + // or after it. + if(extension & 4) { + return read(base + base_displacement) + index + outer_displacement; + } else { + return read(base + base_displacement + index) + outer_displacement; + } } template @@ -248,6 +301,10 @@ Executor::State::calculate_effective_address(Preinstruction i } ea.requires_fetch = false; break; + case AddressingMode::ExtensionWord: + ea.value.l = read_pc(); + ea.requires_fetch = false; + break; // // Absolute addresses. @@ -297,7 +354,7 @@ Executor::State::calculate_effective_address(Preinstruction i ea.requires_fetch = true; break; case AddressingMode::AddressRegisterIndirectWithIndex8bitDisplacement: - ea.value.l = An(instruction.reg(index)).l + index_8bitdisplacement(); + ea.value.l = index_8bitdisplacement(An(instruction.reg(index)).l); ea.requires_fetch = true; break; @@ -305,11 +362,12 @@ Executor::State::calculate_effective_address(Preinstruction i // PC-relative addresses. // case AddressingMode::ProgramCounterIndirectWithDisplacement: - ea.value.l = program_counter.l + int16_t(read_pc()); + ea.value.l = program_counter.l; + ea.value.l += int16_t(read_pc()); ea.requires_fetch = true; break; case AddressingMode::ProgramCounterIndirectWithIndex8bitDisplacement: - ea.value.l = program_counter.l + index_8bitdisplacement(); + ea.value.l = index_8bitdisplacement(program_counter.l); ea.requires_fetch = true; break; @@ -690,6 +748,3 @@ void Executor::State::movem_toR(Preinstruction instruction, u #undef AccessException } -} - -#endif /* InstructionSets_M68k_ExecutorImplementation_hpp */ diff --git a/InstructionSets/M68k/Implementation/InstructionOperandFlags.hpp b/InstructionSets/M68k/Implementation/InstructionOperandFlags.hpp index ca29cc574..008982e18 100644 --- a/InstructionSets/M68k/Implementation/InstructionOperandFlags.hpp +++ b/InstructionSets/M68k/Implementation/InstructionOperandFlags.hpp @@ -6,11 +6,9 @@ // Copyright © 2022 Thomas Harte. All rights reserved. // -#ifndef InstructionSets_68k_InstructionOperandFlags_hpp -#define InstructionSets_68k_InstructionOperandFlags_hpp +#pragma once -namespace InstructionSet { -namespace M68k { +namespace InstructionSet::M68k { template constexpr uint8_t operand_flags(Operation r_operation) { switch((t_operation != Operation::Undefined) ? t_operation : r_operation) { @@ -19,13 +17,29 @@ template constexpr uint8_t operand_flags(Op // // No operands are fetched or stored. - // (which means that source and destination will appear as their effective addresses) + // + // (which means that source and destination, if they exist, + // should be supplied as their effective addresses) // case Operation::PEA: case Operation::JMP: case Operation::JSR: case Operation::MOVEPw: case Operation::MOVEPl: case Operation::TAS: case Operation::RTR: case Operation::RTS: case Operation::RTE: + case Operation::RTM: + case Operation::RTD: + case Operation::TRAP: case Operation::RESET: case Operation::NOP: + case Operation::STOP: case Operation::TRAPV: case Operation::BKPT: + case Operation::TRAPcc: + case Operation::CASb: case Operation::CASw: case Operation::CASl: + case Operation::CAS2w: case Operation::CAS2l: + return 0; + + // + // Operand fetch/store status isn't certain just from the operation; this means + // that further content from an extension word will be required. + // + case Operation::MOVESb: case Operation::MOVESw: case Operation::MOVESl: return 0; // @@ -40,12 +54,17 @@ template constexpr uint8_t operand_flags(Op case Operation::TSTb: case Operation::TSTw: case Operation::TSTl: case Operation::MOVEMtoMw: case Operation::MOVEMtoMl: case Operation::MOVEMtoRw: case Operation::MOVEMtoRl: + case Operation::MOVEtoC: + case Operation::CALLM: + case Operation::CHKorCMP2b: case Operation::CHKorCMP2w: case Operation::CHKorCMP2l: return FetchOp1; // // Single-operand write. // case Operation::MOVEfromUSP: + case Operation::MOVEfromCCR: + case Operation::MOVEfromC: return StoreOp1; // @@ -55,7 +74,7 @@ template constexpr uint8_t operand_flags(Op case Operation::NOTb: case Operation::NOTw: case Operation::NOTl: case Operation::NEGb: case Operation::NEGw: case Operation::NEGl: case Operation::NEGXb: case Operation::NEGXw: case Operation::NEGXl: - case Operation::EXTbtow: case Operation::EXTwtol: + case Operation::EXTbtow: case Operation::EXTwtol: case Operation::EXTbtol: case Operation::SWAP: case Operation::UNLINK: case Operation::ASLm: case Operation::ASRm: @@ -81,33 +100,38 @@ template constexpr uint8_t operand_flags(Op // case Operation::CMPb: case Operation::CMPw: case Operation::CMPl: case Operation::CMPAw: case Operation::CMPAl: - case Operation::CHK: + case Operation::CHKw: case Operation::CHKl: case Operation::BTST: - case Operation::LINKw: + case Operation::LINKw: case Operation::LINKl: + case Operation::BFTST: case Operation::BFFFO: + case Operation::BFEXTU: case Operation::BFEXTS: + case Operation::DIVSorDIVUl: + case Operation::MULSorMULUl: return FetchOp1 | FetchOp2; // // Two-operand; read source, write dest. // - case Operation::MOVEb: case Operation::MOVEw: case Operation::MOVEl: + case Operation::MOVEb: case Operation::MOVEw: case Operation::MOVEl: case Operation::MOVEAw: case Operation::MOVEAl: + case Operation::PACK: case Operation::UNPK: return FetchOp1 | StoreOp2; // // Two-operand; read both, write dest. // case Operation::ABCD: case Operation::SBCD: - case Operation::ADDb: case Operation::ADDw: case Operation::ADDl: + case Operation::ADDb: case Operation::ADDw: case Operation::ADDl: case Operation::ADDAw: case Operation::ADDAl: - case Operation::ADDXb: case Operation::ADDXw: case Operation::ADDXl: - case Operation::SUBb: case Operation::SUBw: case Operation::SUBl: + case Operation::ADDXb: case Operation::ADDXw: case Operation::ADDXl: + case Operation::SUBb: case Operation::SUBw: case Operation::SUBl: case Operation::SUBAw: case Operation::SUBAl: - case Operation::SUBXb: case Operation::SUBXw: case Operation::SUBXl: + case Operation::SUBXb: case Operation::SUBXw: case Operation::SUBXl: case Operation::ORb: case Operation::ORw: case Operation::ORl: case Operation::ANDb: case Operation::ANDw: case Operation::ANDl: case Operation::EORb: case Operation::EORw: case Operation::EORl: - case Operation::DIVU: case Operation::DIVS: - case Operation::MULU: case Operation::MULS: + case Operation::DIVUw: case Operation::DIVSw: + case Operation::MULUw: case Operation::MULSw: case Operation::ASLb: case Operation::ASLw: case Operation::ASLl: case Operation::ASRb: case Operation::ASRw: case Operation::ASRl: case Operation::LSLb: case Operation::LSLw: case Operation::LSLl: @@ -118,6 +142,8 @@ template constexpr uint8_t operand_flags(Op case Operation::ROXRb: case Operation::ROXRw: case Operation::ROXRl: case Operation::BCHG: case Operation::BCLR: case Operation::BSET: + case Operation::BFCHG: case Operation::BFCLR: case Operation::BFSET: + case Operation::BFINS: return FetchOp1 | FetchOp2 | StoreOp2; // @@ -141,6 +167,3 @@ template constexpr uint8_t operand_flags(Op } } -} - -#endif /* InstructionSets_68k_InstructionOperandFlags_hpp */ diff --git a/InstructionSets/M68k/Implementation/InstructionOperandSize.hpp b/InstructionSets/M68k/Implementation/InstructionOperandSize.hpp index 67242e522..63cb03548 100644 --- a/InstructionSets/M68k/Implementation/InstructionOperandSize.hpp +++ b/InstructionSets/M68k/Implementation/InstructionOperandSize.hpp @@ -6,11 +6,9 @@ // Copyright © 2022 Thomas Harte. All rights reserved. // -#ifndef InstructionSets_68k_InstructionOperandSize_hpp -#define InstructionSets_68k_InstructionOperandSize_hpp +#pragma once -namespace InstructionSet { -namespace M68k { +namespace InstructionSet::M68k { template constexpr DataSize operand_size(Operation r_operation) { @@ -22,14 +20,19 @@ constexpr DataSize operand_size(Operation r_operation) { case Operation::STOP: case Operation::RESET: case Operation::RTE: case Operation::RTR: + case Operation::RTD: case Operation::TRAP: case Operation::TRAPV: + case Operation::TRAPcc: + case Operation::BKPT: case Operation::ABCD: case Operation::SBCD: case Operation::NBCD: case Operation::ADDb: case Operation::ADDXb: case Operation::SUBb: case Operation::SUBXb: case Operation::MOVEb: + case Operation::MOVESb: + case Operation::MOVEfromCCR: case Operation::ORItoCCR: case Operation::ANDItoCCR: case Operation::EORItoCCR: @@ -53,12 +56,13 @@ constexpr DataSize operand_size(Operation r_operation) { case Operation::ADDXw: case Operation::SUBw: case Operation::SUBAw: case Operation::SUBXw: case Operation::MOVEw: case Operation::MOVEAw: + case Operation::MOVESw: case Operation::ORItoSR: case Operation::ANDItoSR: case Operation::EORItoSR: case Operation::MOVEtoSR: case Operation::MOVEfromSR: - case Operation::MOVEtoCCR: + case Operation::MOVEtoCCR: // TODO: is this true? case Operation::CMPw: case Operation::CMPAw: case Operation::TSTw: case Operation::DBcc: @@ -80,21 +84,24 @@ constexpr DataSize operand_size(Operation r_operation) { case Operation::MOVEPw: case Operation::ANDw: case Operation::EORw: case Operation::NOTw: case Operation::ORw: - case Operation::DIVU: case Operation::DIVS: - case Operation::MULU: case Operation::MULS: + case Operation::DIVUw: case Operation::DIVSw: + case Operation::MULUw: case Operation::MULSw: case Operation::EXTbtow: case Operation::LINKw: - case Operation::CHK: + case Operation::CHKw: return DataSize::Word; case Operation::ADDl: case Operation::ADDAl: case Operation::ADDXl: case Operation::SUBl: case Operation::SUBAl: case Operation::SUBXl: case Operation::MOVEl: case Operation::MOVEAl: + case Operation::MOVESl: case Operation::LEA: case Operation::PEA: case Operation::EXG: case Operation::SWAP: case Operation::MOVEtoUSP: case Operation::MOVEfromUSP: + case Operation::MOVEtoC: + case Operation::MOVEfromC: case Operation::CMPl: case Operation::CMPAl: case Operation::TSTl: case Operation::JMP: case Operation::JSR: @@ -112,10 +119,11 @@ constexpr DataSize operand_size(Operation r_operation) { case Operation::EXTwtol: case Operation::UNLINK: return DataSize::LongWord; + + default: + // 68020 TODO. + return DataSize::Byte; } } } -} - -#endif /* InstructionSets_68k_InstructionOperandSize_hpp */ diff --git a/InstructionSets/M68k/Implementation/PerformImplementation.hpp b/InstructionSets/M68k/Implementation/PerformImplementation.hpp index 2043cbd9c..2c9143fa1 100644 --- a/InstructionSets/M68k/Implementation/PerformImplementation.hpp +++ b/InstructionSets/M68k/Implementation/PerformImplementation.hpp @@ -6,17 +6,16 @@ // Copyright © 2022 Thomas Harte. All rights reserved. // -#ifndef InstructionSets_M68k_PerformImplementation_h -#define InstructionSets_M68k_PerformImplementation_h +#pragma once +#include "../../../Numeric/Carry.hpp" #include "../ExceptionVectors.hpp" #include #include #include -namespace InstructionSet { -namespace M68k { +namespace InstructionSet::M68k { /// Sign-extend @c x to 32 bits and return as an unsigned 32-bit int. inline uint32_t u_extend16(uint16_t x) { return uint32_t(int16_t(x)); } @@ -26,28 +25,6 @@ inline int32_t s_extend16(uint16_t x) { return int32_t(int16_t(x)); } namespace Primitive { -/// @returns An int of type @c IntT with only the most-significant bit set. -template constexpr IntT top_bit() { - static_assert(!std::numeric_limits::is_signed); - constexpr IntT max = std::numeric_limits::max(); - return max - (max >> 1); -} - -/// @returns An int with the top bit indicating whether overflow occurred when @c source and @c destination -/// were either added (if @c is_add is true) or subtracted (if @c is_add is false) and the result was @c result. -/// All other bits will be clear. -template -static Status::FlagT overflow(IntT source, IntT destination, IntT result) { - const IntT output_changed = result ^ destination; - const IntT input_differed = source ^ destination; - - if constexpr (is_add) { - return top_bit() & output_changed & ~input_differed; - } else { - return top_bit() & output_changed & input_differed; - } -} - /// Performs an add or subtract (as per @c is_add) between @c source and @c destination, /// updating @c status. @c is_extend indicates whether this is an extend operation (e.g. ADDX) /// or a plain one (e.g. ADD). @@ -82,7 +59,7 @@ static void add_sub(IntT source, IntT &destination, Status &status) { status.zero_result = Status::FlagT(result); } status.set_negative(result); - status.overflow_flag = overflow(source, destination, result); + status.overflow_flag = Numeric::overflow(destination, source, result); destination = result; } @@ -144,7 +121,7 @@ void compare(IntT source, IntT destination, Status &status) { const IntT result = destination - source; status.carry_flag = result > destination; status.set_neg_zero(result); - status.overflow_flag = Primitive::overflow(source, destination, result); + status.overflow_flag = Numeric::overflow(destination, source, result); } /// @returns the name of the bit to be used as a mask for BCLR, BCHG, BSET or BTST for @@ -294,7 +271,7 @@ template void negative(IntT &source, Status &sta } status.extend_flag = status.carry_flag = result; // i.e. any value other than 0 will result in carry. status.set_negative(result); - status.overflow_flag = Primitive::overflow(source, IntT(0), result); + status.overflow_flag = Numeric::overflow(IntT(0), source, result); source = result; } @@ -312,11 +289,6 @@ template int shift_count(uint8_t source return count; } -/// @returns The number of bits in @c IntT. -template constexpr int bit_size() { - return sizeof(IntT) * 8; -} - /// Perform an arithmetic or logical shift, i.e. any of LSL, LSR, ASL or ASR. template void shift(uint32_t source, IntT &destination, Status &status, FlowController &flow_controller) { static_assert( @@ -326,7 +298,7 @@ template void shif operation == Operation::LSRb || operation == Operation::LSRw || operation == Operation::LSRl ); - constexpr auto size = bit_size(); + constexpr auto size = Numeric::bit_size(); const auto shift = shift_count(uint8_t(source), flow_controller); if(!shift) { @@ -356,7 +328,7 @@ template void shif if(shift > size) { status.carry_flag = status.extend_flag = 0; } else { - status.carry_flag = status.extend_flag = (destination << (shift - 1)) & top_bit(); + status.carry_flag = status.extend_flag = (destination << (shift - 1)) & Numeric::top_bit(); } if(type == Type::LSL) { @@ -371,7 +343,7 @@ template void shif // For a shift of n places, overflow will be set if the top n+1 bits were not // all the same value. const auto affected_bits = IntT( - ~((top_bit() >> shift) - 1) + ~((Numeric::top_bit() >> shift) - 1) ); // e.g. shift = 1 => ~((0x80 >> 1) - 1) = ~(0x40 - 1) = ~0x3f = 0xc0, i.e. if shift is // 1 then the top two bits are relevant to whether there was overflow. If they have the // same value, i.e. are both 0 or are both 1, then there wasn't. Otherwise there was. @@ -397,7 +369,7 @@ template void shif const IntT sign_word = type == Type::LSR ? - 0 : (destination & top_bit() ? IntT(~0) : 0); + 0 : (destination & Numeric::top_bit() ? IntT(~0) : 0); if(shift >= size) { destination = sign_word; @@ -418,7 +390,7 @@ template void rota operation == Operation::RORb || operation == Operation::RORw || operation == Operation::RORl ); - constexpr auto size = bit_size(); + constexpr auto size = Numeric::bit_size(); auto shift = shift_count(uint8_t(source), flow_controller); if(!shift) { @@ -443,7 +415,7 @@ template void rota (destination << (size - shift)) ); } - status.carry_flag = Status::FlagT(destination & top_bit()); + status.carry_flag = Status::FlagT(destination & Numeric::top_bit()); break; } } @@ -459,20 +431,20 @@ template void rox( operation == Operation::ROXRb || operation == Operation::ROXRw || operation == Operation::ROXRl ); - constexpr auto size = bit_size(); + constexpr auto size = Numeric::bit_size(); auto shift = shift_count(uint8_t(source), flow_controller) % (size + 1); if(!shift) { // When shift is zero, extend is unaffected but is copied to carry. status.carry_flag = status.extend_flag; } else { - switch(operation) { + switch(operation) { case Operation::ROXLb: case Operation::ROXLw: case Operation::ROXLl: status.carry_flag = Status::FlagT((destination >> (size - shift)) & 1); - if(shift == bit_size()) { + if(shift == Numeric::bit_size()) { destination = IntT( - (status.extend_flag ? top_bit() : 0) | + (status.extend_flag ? Numeric::top_bit() : 0) | (destination >> 1) ); } else if(shift == 1) { @@ -492,7 +464,7 @@ template void rox( case Operation::ROXRb: case Operation::ROXRw: case Operation::ROXRl: status.carry_flag = Status::FlagT(destination & (1 << (shift - 1))); - if(shift == bit_size()) { + if(shift == Numeric::bit_size()) { destination = IntT( (status.extend_flag ? 1 : 0) | (destination << 1) @@ -500,12 +472,12 @@ template void rox( } else if(shift == 1) { destination = IntT( (destination >> 1) | - (status.extend_flag ? top_bit() : 0) + (status.extend_flag ? Numeric::top_bit() : 0) ); } else { destination = IntT( (destination >> shift) | - ((status.extend_flag ? top_bit() : 0) >> (shift - 1)) | + ((status.extend_flag ? Numeric::top_bit() : 0) >> (shift - 1)) | (destination << (size + 1 - shift)) ); } @@ -742,15 +714,15 @@ template < Multiplications. */ - case Operation::MULU: Primitive::multiply(src.w, dest.l, status, flow_controller); break; - case Operation::MULS: Primitive::multiply(src.w, dest.l, status, flow_controller); break; + case Operation::MULUw: Primitive::multiply(src.w, dest.l, status, flow_controller); break; + case Operation::MULSw: Primitive::multiply(src.w, dest.l, status, flow_controller); break; /* Divisions. */ - case Operation::DIVU: Primitive::divide(src.w, dest.l, status, flow_controller); break; - case Operation::DIVS: Primitive::divide(src.w, dest.l, status, flow_controller); break; + case Operation::DIVUw: Primitive::divide(src.w, dest.l, status, flow_controller); break; + case Operation::DIVSw: Primitive::divide(src.w, dest.l, status, flow_controller); break; // TRAP, which is a nicer form of ILLEGAL. case Operation::TRAP: @@ -763,7 +735,7 @@ template < } } break; - case Operation::CHK: { + case Operation::CHKw: { const bool is_under = s_extend16(dest.w) < 0; const bool is_over = s_extend16(dest.w) > s_extend16(src.w); @@ -883,14 +855,14 @@ template < Shifts and rotates. */ case Operation::ASLm: - status.extend_flag = status.carry_flag = src.w & Primitive::top_bit(); - status.overflow_flag = (src.w ^ (src.w << 1)) & Primitive::top_bit(); + status.extend_flag = status.carry_flag = src.w & Numeric::top_bit(); + status.overflow_flag = (src.w ^ (src.w << 1)) & Numeric::top_bit(); src.w <<= 1; status.set_neg_zero(src.w); break; case Operation::LSLm: - status.extend_flag = status.carry_flag = src.w & Primitive::top_bit(); + status.extend_flag = status.carry_flag = src.w & Numeric::top_bit(); status.overflow_flag = 0; src.w <<= 1; status.set_neg_zero(src.w); @@ -899,7 +871,7 @@ template < case Operation::ASRm: status.extend_flag = status.carry_flag = src.w & 1; status.overflow_flag = 0; - src.w = (src.w & Primitive::top_bit()) | (src.w >> 1); + src.w = (src.w & Numeric::top_bit()) | (src.w >> 1); status.set_neg_zero(src.w); break; @@ -919,13 +891,13 @@ template < case Operation::RORm: src.w = uint16_t((src.w >> 1) | (src.w << 15)); - status.carry_flag = src.w & Primitive::top_bit(); + status.carry_flag = src.w & Numeric::top_bit(); status.overflow_flag = 0; status.set_neg_zero(src.w); break; case Operation::ROXLm: - status.carry_flag = src.w & Primitive::top_bit(); + status.carry_flag = src.w & Numeric::top_bit(); src.w = uint16_t((src.w << 1) | (status.extend_flag ? 0x0001 : 0x0000)); status.extend_flag = status.carry_flag; status.overflow_flag = 0; @@ -1031,6 +1003,3 @@ template < } } -} - -#endif /* InstructionSets_M68k_PerformImplementation_h */ diff --git a/InstructionSets/M68k/Instruction.cpp b/InstructionSets/M68k/Instruction.cpp index dfd09c4fd..3031e5d79 100644 --- a/InstructionSets/M68k/Instruction.cpp +++ b/InstructionSets/M68k/Instruction.cpp @@ -45,6 +45,7 @@ std::string Preinstruction::operand_description(int index, int opcode) const { case AddressingMode::AbsoluteLong: return "(xxx).l"; + case AddressingMode::ExtensionWord: case AddressingMode::ImmediateData: return "#"; @@ -95,14 +96,21 @@ const char *_to_string(Operation operation, bool is_quick) { case Operation::MOVEAw: return "MOVEA.w"; case Operation::MOVEAl: return "MOVEA.l"; + case Operation::MOVESb: return "MOVES.b"; + case Operation::MOVESw: return "MOVES.w"; + case Operation::MOVESl: return "MOVES.l"; + case Operation::LEA: return "LEA"; case Operation::PEA: return "PEA"; case Operation::MOVEtoSR: return "MOVEtoSR"; case Operation::MOVEfromSR: return "MOVEfromSR"; case Operation::MOVEtoCCR: return "MOVEtoCCR"; + case Operation::MOVEfromCCR: return "MOVEfromCCR"; case Operation::MOVEtoUSP: return "MOVEtoUSP"; case Operation::MOVEfromUSP: return "MOVEfromUSP"; + case Operation::MOVEtoC: return "MOVEtoC"; + case Operation::MOVEfromC: return "MOVEfromC"; case Operation::ORItoSR: return "ORItoSR"; case Operation::ORItoCCR: return "ORItoCCR"; @@ -130,8 +138,12 @@ const char *_to_string(Operation operation, bool is_quick) { case Operation::JMP: return "JMP"; case Operation::JSR: return "JSR"; case Operation::RTS: return "RTS"; + case Operation::RTD: return "RTD"; + case Operation::RTM: return "RTM"; + case Operation::DBcc: return "DBcc"; case Operation::Scc: return "Scc"; + case Operation::TRAPcc: return "TRAPcc"; case Operation::Bccb: case Operation::Bccl: @@ -141,6 +153,13 @@ const char *_to_string(Operation operation, bool is_quick) { case Operation::BSRl: case Operation::BSRw: return "BSR"; + case Operation::CASb: return "CAS.b"; + case Operation::CASw: return "CAS.w"; + case Operation::CASl: return "CAS.l"; + + case Operation::CAS2w: return "CAS2.w"; + case Operation::CAS2l: return "CAS2.l"; + case Operation::CLRb: return "CLR.b"; case Operation::CLRw: return "CLR.w"; case Operation::CLRl: return "CLR.l"; @@ -217,17 +236,26 @@ const char *_to_string(Operation operation, bool is_quick) { case Operation::ORw: return "OR.w"; case Operation::ORl: return "OR.l"; - case Operation::MULU: return "MULU"; - case Operation::MULS: return "MULS"; - case Operation::DIVU: return "DIVU"; - case Operation::DIVS: return "DIVS"; + case Operation::MULUw: return "MULU"; + case Operation::MULSw: return "MULS"; + case Operation::MULSorMULUl: return "[MULS/MULU]{L}.l"; + + case Operation::DIVUw: return "DIVU"; + case Operation::DIVSw: return "DIVS"; + case Operation::DIVSorDIVUl: return "[DIVS/DIVU]{L}.l"; case Operation::RTE: return "RTE"; case Operation::RTR: return "RTR"; case Operation::TRAP: return "TRAP"; case Operation::TRAPV: return "TRAPV"; - case Operation::CHK: return "CHK"; + + case Operation::CHKw: return "CHK"; + case Operation::CHKl: return "CHK.l"; + + case Operation::CHKorCMP2b: return "[CHK/CMP]2.b"; + case Operation::CHKorCMP2w: return "[CHK/CMP]2.w"; + case Operation::CHKorCMP2l: return "[CHK/CMP]2.l"; case Operation::EXG: return "EXG"; case Operation::SWAP: return "SWAP"; @@ -236,13 +264,29 @@ const char *_to_string(Operation operation, bool is_quick) { case Operation::EXTbtow: return "EXT.w"; case Operation::EXTwtol: return "EXT.l"; + case Operation::EXTbtol: return "EXTB.l"; case Operation::LINKw: return "LINK"; + case Operation::LINKl: return "LINK.l"; case Operation::UNLINK: return "UNLINK"; case Operation::STOP: return "STOP"; case Operation::RESET: return "RESET"; + case Operation::BKPT: return "BKPT"; + + case Operation::BFCHG: return "BFCHG"; + case Operation::BFCLR: return "BFCLR"; + case Operation::BFEXTS: return "BFEXTS"; + case Operation::BFEXTU: return "BFEXTU"; + case Operation::BFFFO: return "BFFFO"; + case Operation::BFINS: return "BFINS"; + case Operation::BFSET: return "BFSET"; + case Operation::BFTST: return "BFTST"; + + case Operation::PACK: return "PACK"; + case Operation::UNPK: return "UNPK"; + default: assert(false); return "???"; @@ -268,6 +312,9 @@ std::string Preinstruction::to_string(int opcode) const { if(!operand1.empty()) result += std::string(" ") + operand1; if(!operand2.empty()) result += std::string(", ") + operand2; + const int extension_words = additional_extension_words(); + if(extension_words) result += std::string(" [+") + std::to_string(extension_words) + "]"; + return result; } diff --git a/InstructionSets/M68k/Instruction.hpp b/InstructionSets/M68k/Instruction.hpp index de3871762..b5e601930 100644 --- a/InstructionSets/M68k/Instruction.hpp +++ b/InstructionSets/M68k/Instruction.hpp @@ -6,8 +6,7 @@ // Copyright © 2022 Thomas Harte. All rights reserved. // -#ifndef InstructionSets_68k_Instruction_hpp -#define InstructionSets_68k_Instruction_hpp +#pragma once #include "Model.hpp" @@ -15,12 +14,15 @@ #include #include -namespace InstructionSet { -namespace M68k { +namespace InstructionSet::M68k { enum class Operation: uint8_t { Undefined, + // + // 68000 operations. + // + NOP, ABCD, SBCD, NBCD, @@ -57,8 +59,8 @@ enum class Operation: uint8_t { DBcc, Scc, - Bccb, Bccw, Bccl, - BSRb, BSRw, BSRl, + Bccb, Bccw, + BSRb, BSRw, CLRb, CLRw, CLRl, NEGXb, NEGXw, NEGXl, @@ -80,16 +82,16 @@ enum class Operation: uint8_t { ANDb, ANDw, ANDl, EORb, EORw, EORl, - NOTb, NOTw, NOTl, + NOTb, NOTw, NOTl, ORb, ORw, ORl, - MULU, MULS, - DIVU, DIVS, + MULUw, MULSw, + DIVUw, DIVSw, RTE, RTR, TRAP, TRAPV, - CHK, + CHKw, EXG, SWAP, @@ -101,7 +103,102 @@ enum class Operation: uint8_t { STOP, RESET, - Max = RESET + // + // 68010 additions. + // + + MOVEfromCCR, + MOVEtoC, MOVEfromC, + MOVESb, MOVESw, MOVESl, + BKPT, RTD, + + // + // 68020 additions. + // + + TRAPcc, + + CALLM, RTM, + + BFCHG, BFCLR, + BFEXTS, BFEXTU, + BFFFO, BFINS, + BFSET, BFTST, + + PACK, UNPK, + + CASb, CASw, CASl, + CAS2w, CAS2l, + + // CHK2 and CMP2 are distinguished by their extension word; + // since this code deals in Preinstructions, i.e. as much + // as can be derived from the instruction word alone, in addition + // to the full things, the following enums result. + CHKorCMP2b, CHKorCMP2w, CHKorCMP2l, + + // DIVS.l, DIVSL.l, DIVU.l and DIVUL.l are all distinguishable + // only by the extension word. + DIVSorDIVUl, + + // MULS.l, MULSL.l, MULU.l and MULUL.l are all distinguishable + // only by the extension word. + MULSorMULUl, + + Bccl, BSRl, + LINKl, CHKl, + + EXTbtol, + + // Coprocessor instructions are omitted for now, until I can + // determine by what mechanism the number of + // "OPTIONAL COPROCESSOR-DEFINED EXTENSION WORDS" is determined. +// cpBcc, cpDBcc, cpGEN, +// cpScc, cpTRAPcc, cpRESTORE, +// cpSAVE, + + // + // 68030 additions. + // + + PFLUSH, PFLUSHA, + PLOADR, PLOADW, + PMOVE, PMOVEFD, + PTESTR, PTESTW, + + // + // 68040 additions. + // + + // TODO: the big addition of the 68040 is incorporation of the FPU; should I make decoding of those instructions + // dependent upon a 68040 being selected, or should I offer a separate decoder in order to support systems with + // a coprocessor? + + // + // Introspection. + // + Max68000 = RESET, + Max68010 = RTD, + Max68020 = EXTbtol, + Max68030 = PTESTW, + Max68040 = PTESTW, +}; + +// Provide per-model max entries in Operation. +template struct OperationMax {}; +template <> struct OperationMax { + static constexpr Operation value = Operation::Max68000; +}; +template <> struct OperationMax { + static constexpr Operation value = Operation::Max68010; +}; +template <> struct OperationMax { + static constexpr Operation value = Operation::Max68020; +}; +template <> struct OperationMax { + static constexpr Operation value = Operation::Max68030; +}; +template <> struct OperationMax { + static constexpr Operation value = Operation::Max68040; }; const char *to_string(Operation op); @@ -118,6 +215,7 @@ constexpr bool requires_supervisor(Operation op) { case Operation::EORItoSR: case Operation::RTE: case Operation::RESET: case Operation::STOP: case Operation::MOVEtoUSP: case Operation::MOVEfromUSP: + case Operation::MOVEtoC: case Operation::MOVEfromC: case Operation::MOVEtoSR: return true; @@ -147,6 +245,7 @@ constexpr uint32_t quick(uint16_t instruction, Operation r_op = Operation::Undef case Operation::BSRb: case Operation::MOVEl: return uint32_t(int8_t(instruction)); case Operation::TRAP: return uint32_t(instruction & 15); + case Operation::BKPT: return uint32_t(instruction & 7); default: { uint32_t value = (instruction >> 9) & 7; value |= (value - 1)&8; @@ -189,7 +288,7 @@ enum class Condition { /// Those entries starting 0b00 or 0b01 are mapped as per the 68000's native encoding; /// those starting 0b00 are those which are indicated directly by a mode field and those starting /// 0b01 are those which are indicated by a register field given a mode of 0b111. The only minor -/// exception is AddressRegisterDirect, which exists on a 68000 but isn't specifiable by a +/// exception is AddressRegisterDirect, which exists on a 68000 but isn't specifiable by a /// mode and register, it's contextual based on the instruction. /// /// Those modes starting in 0b10 are the various extended addressing modes introduced as @@ -249,6 +348,10 @@ enum class AddressingMode: uint8_t { /// # ImmediateData = 0b01'100, + /// An additional word of data. Differs from ImmediateData by being + /// a fixed size, rather than the @c operand_size of the operation. + ExtensionWord = 0b01'111, + /// .q; value is embedded in the opcode. Quick = 0b01'110, }; @@ -308,14 +411,32 @@ class Preinstruction { return operands_[index] & 0xf; } + /// @returns @c true if this instruction requires supervisor privileges; @c false otherwise. bool requires_supervisor() const { - return flags_ & 0x80; + return flags_ & Flags::IsSupervisor; } + /// @returns @c true if this instruction will require further fetching than can be encoded in a + /// @c Preinstruction. In practice this means it is one of a very small quantity of 68020+ + /// instructions; those that can rationalise extension words into one of the two operands will + /// do so. Use the free function @c extension_words(instruction.operation) to + /// look up the number of additional words required. + /// + /// (specifically affected, at least: PACK, UNPK, CAS, CAS2) + bool requires_further_extension() const { + return flags_ & Flags::RequiresFurtherExtension; + } + /// @returns The number of additional extension words required, beyond those encoded as operands. + int additional_extension_words() const { + return flags_ & Flags::RequiresFurtherExtension ? (flags_ & Flags::ConditionMask) >> Flags::ConditionShift : 0; + } + /// @returns The @c DataSize used for operands of this instruction, i.e. byte, word or longword. DataSize operand_size() const { - return DataSize(flags_ & 0x03); + return DataSize((flags_ & Flags::SizeMask) >> Flags::SizeShift); } + /// @returns The condition code evaluated by this instruction if applicable. If this instruction is not + /// conditional, the result is undefined. Condition condition() const { - return Condition((flags_ >> 2) & 0x0f); + return Condition((flags_ & Flags::ConditionMask) >> Flags::ConditionShift); } private: @@ -330,18 +451,33 @@ class Preinstruction { AddressingMode op1_mode, int op1_reg, AddressingMode op2_mode, int op2_reg, bool is_supervisor, + int extension_words, DataSize size, Condition condition) : operation(operation) { operands_[0] = uint8_t((uint8_t(op1_mode) << 3) | op1_reg); operands_[1] = uint8_t((uint8_t(op2_mode) << 3) | op2_reg); flags_ = uint8_t( - (is_supervisor ? 0x80 : 0x00) | - (int(condition) << 2) | - int(size) + (is_supervisor ? Flags::IsSupervisor : 0x00) | + (extension_words ? Flags::RequiresFurtherExtension : 0x00) | + (int(condition) << Flags::ConditionShift) | + (extension_words << Flags::ConditionShift) | + (int(size) << Flags::SizeShift) ); } + struct Flags { + static constexpr uint8_t IsSupervisor = 0b1000'0000; + static constexpr uint8_t RequiresFurtherExtension = 0b0100'0000; + static constexpr uint8_t ConditionMask = 0b0011'1100; + static constexpr uint8_t SizeMask = 0b0000'0011; + + static constexpr int IsSupervisorShift = 7; + static constexpr int RequiresFurtherExtensionShift = 6; + static constexpr int ConditionShift = 2; + static constexpr int SizeShift = 0; + }; + Preinstruction() {} /// Produces a string description of this instruction; if @c opcode @@ -356,10 +492,7 @@ class Preinstruction { const char *operation_string() const; }; -} } #include "Implementation/InstructionOperandSize.hpp" #include "Implementation/InstructionOperandFlags.hpp" - -#endif /* InstructionSets_68k_Instruction_hpp */ diff --git a/InstructionSets/M68k/Model.hpp b/InstructionSets/M68k/Model.hpp index 3de18d405..ef3444c2b 100644 --- a/InstructionSets/M68k/Model.hpp +++ b/InstructionSets/M68k/Model.hpp @@ -6,11 +6,9 @@ // Copyright © 2022 Thomas Harte. All rights reserved. // -#ifndef InstructionSets_M68k_Model_hpp -#define InstructionSets_M68k_Model_hpp +#pragma once -namespace InstructionSet { -namespace M68k { +namespace InstructionSet::M68k { enum class Model { M68000, @@ -21,6 +19,3 @@ enum class Model { }; } -} - -#endif /* InstructionSets_M68k_Model_hpp */ diff --git a/InstructionSets/M68k/Perform.hpp b/InstructionSets/M68k/Perform.hpp index 300706b00..e32ea3ed3 100644 --- a/InstructionSets/M68k/Perform.hpp +++ b/InstructionSets/M68k/Perform.hpp @@ -6,16 +6,14 @@ // Copyright © 2022 Thomas Harte. All rights reserved. // -#ifndef InstructionSets_M68k_Perform_h -#define InstructionSets_M68k_Perform_h +#pragma once #include "Model.hpp" #include "Instruction.hpp" #include "Status.hpp" #include "../../Numeric/RegisterSizes.hpp" -namespace InstructionSet { -namespace M68k { +namespace InstructionSet::M68k { struct NullFlowController { // @@ -167,9 +165,6 @@ template < Operation operation = Operation::Undefined > void perform(Preinstruction instruction, CPU::RegisterPair32 &source, CPU::RegisterPair32 &dest, Status &status, FlowController &flow_controller); -} } #include "Implementation/PerformImplementation.hpp" - -#endif /* InstructionSets_M68k_Perform_h */ diff --git a/InstructionSets/M68k/RegisterSet.hpp b/InstructionSets/M68k/RegisterSet.hpp index 87e98f70b..984fcafed 100644 --- a/InstructionSets/M68k/RegisterSet.hpp +++ b/InstructionSets/M68k/RegisterSet.hpp @@ -6,11 +6,9 @@ // Copyright © 2022 Thomas Harte. All rights reserved. // -#ifndef InstructionSets_M68k_RegisterSet_h -#define InstructionSets_M68k_RegisterSet_h +#pragma once -namespace InstructionSet { -namespace M68k { +namespace InstructionSet::M68k { struct RegisterSet { uint32_t data[8], address[7]; @@ -26,6 +24,3 @@ struct RegisterSet { }; } -} - -#endif /* InstructionSets_M68k_RegisterSet_h */ diff --git a/InstructionSets/M68k/Status.hpp b/InstructionSets/M68k/Status.hpp index 22f15c52d..49ff17cc4 100644 --- a/InstructionSets/M68k/Status.hpp +++ b/InstructionSets/M68k/Status.hpp @@ -6,13 +6,11 @@ // Copyright © 2022 Thomas Harte. All rights reserved. // -#ifndef InstructionSets_M68k_Status_h -#define InstructionSets_M68k_Status_h +#pragma once #include "Instruction.hpp" -namespace InstructionSet { -namespace M68k { +namespace InstructionSet::M68k { namespace ConditionCode { @@ -69,7 +67,7 @@ struct Status { /// Gets the current condition codes. constexpr uint16_t ccr() const { return - (carry_flag ? ConditionCode::Carry : 0) | + (carry_flag ? ConditionCode::Carry : 0) | (overflow_flag ? ConditionCode::Overflow : 0) | (!zero_result ? ConditionCode::Zero : 0) | (negative_flag ? ConditionCode::Negative : 0) | @@ -160,6 +158,3 @@ struct Status { }; } -} - -#endif /* InstructionSets_M68k_Status_h */ diff --git a/InstructionSets/PowerPC/Decoder.cpp b/InstructionSets/PowerPC/Decoder.cpp index bcb619866..3c0c1796e 100644 --- a/InstructionSets/PowerPC/Decoder.cpp +++ b/InstructionSets/PowerPC/Decoder.cpp @@ -326,7 +326,7 @@ Instruction Decoder::decode(uint32_t opcode) { Bind(Six(0b001010), cmpli); Bind(Six(0b001011), cmpi); } - + // Second pass: all those with a top six bits and a bottom nine or ten. switch(opcode & SixTen(0b111111, 0b1111111111)) { default: break; diff --git a/InstructionSets/PowerPC/Decoder.hpp b/InstructionSets/PowerPC/Decoder.hpp index ac763217e..7443f8ab0 100644 --- a/InstructionSets/PowerPC/Decoder.hpp +++ b/InstructionSets/PowerPC/Decoder.hpp @@ -6,13 +6,11 @@ // Copyright © 2020 Thomas Harte. All rights reserved. // -#ifndef InstructionSets_PowerPC_Decoder_hpp -#define InstructionSets_PowerPC_Decoder_hpp +#pragma once #include "Instruction.hpp" -namespace InstructionSet { -namespace PowerPC { +namespace InstructionSet::PowerPC { enum class Model { /// i.e. 32-bit, with POWER carry-over instructions. @@ -51,6 +49,3 @@ template struct Decoder { }; } -} - -#endif /* InstructionSets_PowerPC_Decoder_hpp */ diff --git a/InstructionSets/PowerPC/Instruction.hpp b/InstructionSets/PowerPC/Instruction.hpp index 07d9b0175..7116d3a0b 100644 --- a/InstructionSets/PowerPC/Instruction.hpp +++ b/InstructionSets/PowerPC/Instruction.hpp @@ -6,13 +6,11 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef InstructionSets_PowerPC_Instruction_h -#define InstructionSets_PowerPC_Instruction_h +#pragma once #include -namespace InstructionSet { -namespace PowerPC { +namespace InstructionSet::PowerPC { enum class CacheLine: uint32_t { Instruction = 0b01100, @@ -47,13 +45,13 @@ enum class BranchOption: uint32_t { // condition ending Set or Clear => test the condition bit. Dec_NotZeroAndClear = 0b0000, Dec_ZeroAndClear = 0b0001, - Clear = 0b0010, + Clear = 0b0010, Dec_NotZeroAndSet = 0b0100, - Dec_ZeroAndSet = 0b0101, - Set = 0b0110, - Dec_NotZero = 0b1000, + Dec_ZeroAndSet = 0b0101, + Set = 0b0110, + Dec_NotZero = 0b1000, Dec_Zero = 0b1001, - Always = 0b1010, + Always = 0b1010, }; @@ -1390,30 +1388,30 @@ struct Instruction { int32_t imm() const { return (opcode >> 12) & 0xf; } /// Specifies the conditions on which to trap. - int32_t to() const { return (opcode >> 21) & 0x1f; } + int32_t to() const { return (opcode >> 21) & 0x1f; } /// Register source A or destination. - uint32_t rA() const { return (opcode >> 16) & 0x1f; } + uint32_t rA() const { return (opcode >> 16) & 0x1f; } /// Register source B. - uint32_t rB() const { return (opcode >> 11) & 0x1f; } + uint32_t rB() const { return (opcode >> 11) & 0x1f; } /// Register destination. - uint32_t rD() const { return (opcode >> 21) & 0x1f; } + uint32_t rD() const { return (opcode >> 21) & 0x1f; } /// Register source. - uint32_t rS() const { return (opcode >> 21) & 0x1f; } + uint32_t rS() const { return (opcode >> 21) & 0x1f; } /// Floating point register source A. - uint32_t frA() const { return (opcode >> 16) & 0x1f; } + uint32_t frA() const { return (opcode >> 16) & 0x1f; } /// Floating point register source B. - uint32_t frB() const { return (opcode >> 11) & 0x1f; } + uint32_t frB() const { return (opcode >> 11) & 0x1f; } /// Floating point register source C. - uint32_t frC() const { return (opcode >> 6) & 0x1f; } + uint32_t frC() const { return (opcode >> 6) & 0x1f; } /// Floating point register source. - uint32_t frS() const { return (opcode >> 21) & 0x1f; } + uint32_t frS() const { return (opcode >> 21) & 0x1f; } /// Floating point register destination. - uint32_t frD() const { return (opcode >> 21) & 0x1f; } + uint32_t frD() const { return (opcode >> 21) & 0x1f; } /// Branch conditional options as per PowerPC spec, i.e. options + branch-prediction flag. - uint32_t bo() const { return (opcode >> 21) & 0x1f; } + uint32_t bo() const { return (opcode >> 21) & 0x1f; } /// Just the branch options, with the branch prediction flag severed. BranchOption branch_options() const { return BranchOption((opcode >> 22) & 0xf); @@ -1423,7 +1421,7 @@ struct Instruction { return opcode & 0x200000; } /// Source condition register bit for branch conditionals. - uint32_t bi() const { return (opcode >> 16) & 0x1f; } + uint32_t bi() const { return (opcode >> 16) & 0x1f; } /// Branch displacement; provided as already sign extended. int16_t bd() const { return int16_t(opcode & 0xfffc); } @@ -1448,9 +1446,9 @@ struct Instruction { /// Provides the mask described by 32-bit rotate operations. /// /// Per IBM's rules: - /// mb < me+1 => set [mb, me] - /// mb == me+1 => set all bits - /// mb > me+1 => complement of set [me+1, mb-1] + /// mb < me+1 => set [mb, me] + /// mb == me+1 => set all bits + /// mb > me+1 => complement of set [me+1, mb-1] template IntT rotate_mask() const { const auto mb_bit = mb(); const auto me_bit = me(); @@ -1522,15 +1520,12 @@ struct Instruction { /// Identifies a special purpose register. - uint32_t spr() const { return (opcode >> 11) & 0x3ff; } + uint32_t spr() const { return (opcode >> 11) & 0x3ff; } /// Identifies a time base register. - uint32_t tbr() const { return (opcode >> 11) & 0x3ff; } + uint32_t tbr() const { return (opcode >> 11) & 0x3ff; } }; // Sanity check on Instruction size. static_assert(sizeof(Instruction) <= 8); } -} - -#endif /* InstructionSets_PowerPC_Instruction_h */ diff --git a/InstructionSets/README.md b/InstructionSets/README.md index 80db78f44..fc50a84ea 100644 --- a/InstructionSets/README.md +++ b/InstructionSets/README.md @@ -4,15 +4,15 @@ Code in here provides the means to disassemble, and to execute code for certain It **does not seek to emulate specific processors** other than in terms of implementing their instruction sets. So: * it doesn't involve itself in the actual bus signalling of real processors; and -* instruction-level timing (e.g. total cycle counts) may be unimplemented, and is likely to be incomplete. +* instruction-level timing (e.g. total cycle counts) may be unimplemented, and is likely to be incomplete. This part of CLK is intended primarily to provide disassembly services for static analysis, and processing for machines where timing is not part of the specification — i.e. anything that's an instruction set and a HAL. ## Decoders -A decoder extracts fully-decoded instructions from a data stream for its associated architecture. +A decoder extracts fully-decoded instructions from a data stream for its associated architecture. -The meaning of 'fully-decoded' is flexible but it means that a caller can easily discern at least: +The meaning of 'fully-decoded' is flexible but it means that a caller can easily discern at least: * the operation in use; * its addressing mode; and * relevant registers. @@ -23,7 +23,7 @@ In deciding what to expose, what to store ahead of time and what to obtain just- 1. disassemblers; and 2. instruction executors. -It may also be reasonable to make allowances for bus-centric CPU emulators, but those will be tightly coupled to specific decoders so no general rules need apply. +It may also be reasonable to make allowances for bus-centric CPU emulators, but those will be tightly coupled to specific decoders so no general rules need apply. Disassemblers are likely to decode an instruction, output it, and then immediately forget about it. @@ -31,7 +31,7 @@ Instruction executors may opt to cache decoded instructions to reduce recurrent ### Likely Interfaces -These examples assume that the processor itself doesn't hold any state that affects instruction parsing. Whether processors with such state offer more than one decoder or take state as an argument will be a question of measure and effect. +These examples assume that the processor itself doesn't hold any state that affects instruction parsing. Whether processors with such state offer more than one decoder or take state as an argument will be a question of measure and effect. #### Fixed-size instruction words @@ -53,7 +53,7 @@ In this sample the returned pair provides an `int` size that is one of: * a positive number, indicating a completed decoding that consumed that many `word_type`s; or * a negative number, indicating the [negatived] minimum number of `word_type`s that the caller should try to get hold of before calling `decode` again. -A caller is permitted to react in any way it prefers to negative numbers; they're a hint potentially to reduce calling overhead only. A size of `0` would be taken to have the same meaning as a size of `-1`. +A caller is permitted to react in any way it prefers to negative numbers; they're a hint potentially to reduce calling overhead only. A size of `0` would be taken to have the same meaning as a size of `-1`. ## Parsers @@ -81,6 +81,6 @@ An executor is assumed to bundle all the things that go into instruction set exe ## Caching Executor -The caching executor is a generic class templated on a specific executor. It will use an executor to cache the results of parsing. +The caching executor is a generic class templated on a specific executor. It will use an executor to cache the results of parsing. -Idiomatically, the objects that perform instructions will expect to receive an appropriate executor as an argument. If they require other information, such as a copy of the decoded instruction, it should be built into the classes. +Idiomatically, the objects that perform instructions will expect to receive an appropriate executor as an argument. If they require other information, such as a copy of the decoded instruction, it should be built into the classes. diff --git a/InstructionSets/x86/AccessType.hpp b/InstructionSets/x86/AccessType.hpp new file mode 100644 index 000000000..3142f7c4b --- /dev/null +++ b/InstructionSets/x86/AccessType.hpp @@ -0,0 +1,63 @@ +// +// AccessType.hpp +// Clock Signal +// +// Created by Thomas Harte on 05/11/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +namespace InstructionSet::x86 { + +/// Explains the type of access that `perform` intends to perform; is provided as a template parameter to whatever +/// the caller supplies as `MemoryT` and `RegistersT` when obtaining a reference to whatever the processor +/// intends to reference. +/// +/// `perform` guarantees to validate all accesses before modifying any state, giving the caller opportunity to generate +/// any exceptions that might be applicable. +enum class AccessType { + /// The requested value will be read from. + Read, + /// The requested value will be written to. + Write, + /// The requested value will be read from and then written to. + ReadModifyWrite, + /// The requested value has already been authorised for whatever form of access is now intended, so there's no + /// need further to inspect. This is done e.g. by operations that will push multiple values to the stack to verify that + /// all necessary stack space is available ahead of pushing anything, though each individual push will then result in + /// a further `Preauthorised` access. + PreauthorisedRead, +}; + +constexpr bool is_writeable(AccessType type) { + return type == AccessType::ReadModifyWrite || type == AccessType::Write; +} + +template struct Accessor; + +// Reads: return a value directly. +template struct Accessor { using type = IntT; }; +template struct Accessor { using type = IntT; }; + +// Writes: return a custom type that can be written but not read. +template +class Writeable { + public: + Writeable(IntT &target) : target_(target) {} + IntT operator=(IntT value) { return target_ = value; } + private: + IntT &target_; +}; +template struct Accessor { using type = Writeable; }; + +// Read-modify-writes: return a reference. +template struct Accessor { using type = IntT &; }; + +// Shorthands; assumed that preauthorised reads have the same return type as reads. +template using read_t = typename Accessor::type; +template using write_t = typename Accessor::type; +template using modify_t = typename Accessor::type; +template using access_t = typename Accessor::type; + +} diff --git a/InstructionSets/x86/DataPointerResolver.hpp b/InstructionSets/x86/DataPointerResolver.hpp deleted file mode 100644 index 2a2197a25..000000000 --- a/InstructionSets/x86/DataPointerResolver.hpp +++ /dev/null @@ -1,320 +0,0 @@ -// -// DataPointerResolver.hpp -// Clock Signal -// -// Created by Thomas Harte on 24/02/2022. -// Copyright © 2022 Thomas Harte. All rights reserved. -// - -#ifndef DataPointerResolver_hpp -#define DataPointerResolver_hpp - -#include "Instruction.hpp" -#include "Model.hpp" - -#include - -namespace InstructionSet { -namespace x86 { - -/// Unlike source, describes only registers, and breaks -/// them down by conventional name — so AL, AH, AX and EAX are all -/// listed separately and uniquely, rather than being eAX+size or -/// eSPorAH with a size of 1. -enum class Register: uint8_t { - // 8-bit registers. - AL, AH, - CL, CH, - DL, DH, - BL, BH, - - // 16-bit registers. - AX, CX, DX, BX, - SP, BP, SI, DI, - ES, CS, SS, DS, - FS, GS, - - // 32-bit registers. - EAX, ECX, EDX, EBX, - ESP, EBP, ESI, EDI, - - // - None -}; - -/// @returns @c true if @c r is the same size as @c DataT; @c false otherwise. -/// @discussion Provided primarily to aid in asserts; if the decoder and resolver are both -/// working then it shouldn't be necessary to test this in register files. -template constexpr bool is_sized(Register r) { - static_assert(sizeof(DataT) == 4 || sizeof(DataT) == 2 || sizeof(DataT) == 1); - - if constexpr (sizeof(DataT) == 4) { - return r >= Register::EAX && r < Register::None; - } - - if constexpr (sizeof(DataT) == 2) { - return r >= Register::AX && r < Register::EAX; - } - - if constexpr (sizeof(DataT) == 1) { - return r >= Register::AL && r < Register::AX; - } - - return false; -} - -/// @returns the proper @c Register given @c source and data of size @c sizeof(DataT), -/// or Register::None if no such register exists (e.g. asking for a 32-bit version of CS). -template constexpr Register register_for_source(Source source) { - static_assert(sizeof(DataT) == 4 || sizeof(DataT) == 2 || sizeof(DataT) == 1); - - if constexpr (sizeof(DataT) == 4) { - switch(source) { - case Source::eAX: return Register::EAX; - case Source::eCX: return Register::ECX; - case Source::eDX: return Register::EDX; - case Source::eBX: return Register::EBX; - case Source::eSPorAH: return Register::ESP; - case Source::eBPorCH: return Register::EBP; - case Source::eSIorDH: return Register::ESI; - case Source::eDIorBH: return Register::EDI; - - default: break; - } - } - - if constexpr (sizeof(DataT) == 2) { - switch(source) { - case Source::eAX: return Register::AX; - case Source::eCX: return Register::CX; - case Source::eDX: return Register::DX; - case Source::eBX: return Register::BX; - case Source::eSPorAH: return Register::SP; - case Source::eBPorCH: return Register::BP; - case Source::eSIorDH: return Register::SI; - case Source::eDIorBH: return Register::DI; - case Source::ES: return Register::ES; - case Source::CS: return Register::CS; - case Source::SS: return Register::SS; - case Source::DS: return Register::DS; - case Source::FS: return Register::FS; - case Source::GS: return Register::GS; - - default: break; - } - } - - if constexpr (sizeof(DataT) == 1) { - switch(source) { - case Source::eAX: return Register::AL; - case Source::eCX: return Register::CL; - case Source::eDX: return Register::DL; - case Source::eBX: return Register::BL; - case Source::eSPorAH: return Register::AH; - case Source::eBPorCH: return Register::CH; - case Source::eSIorDH: return Register::DH; - case Source::eDIorBH: return Register::BH; - - default: break; - } - } - - return Register::None; -} - -/// Reads from or writes to the source or target identified by a DataPointer, relying upon two user-supplied classes: -/// -/// * a register bank; and -/// * a memory pool. -/// -/// The register bank should implement `template DataT read()` and `template void write(DataT)`. -/// Those functions will be called only with registers and data types that are appropriate to the @c model. -/// -/// The memory pool should implement `template DataT read(Source segment, uint32_t address)` and -/// `template void write(Source segment, uint32_t address, DataT value)`. -template class DataPointerResolver { - public: - public: - /// Reads the data pointed to by @c pointer, referencing @c instruction, @c memory and @c registers as necessary. - template static DataT read( - RegistersT ®isters, - MemoryT &memory, - const Instruction &instruction, - DataPointer pointer); - - /// Writes @c value to the data pointed to by @c pointer, referencing @c instruction, @c memory and @c registers as necessary. - template static void write( - RegistersT ®isters, - MemoryT &memory, - const Instruction &instruction, - DataPointer pointer, - DataT value); - - /// Computes the effective address of @c pointer including any displacement applied by @c instruction. - /// @c pointer must be of type Source::Indirect. - template - static uint32_t effective_address( - RegistersT ®isters, - const Instruction &instruction, - DataPointer pointer); - - private: - template static void access( - RegistersT ®isters, - MemoryT &memory, - const Instruction &instruction, - DataPointer pointer, - DataT &value); -}; - - -// -// Implementation begins here. -// - -template -template DataT DataPointerResolver::read( - RegistersT ®isters, - MemoryT &memory, - const Instruction &instruction, - DataPointer pointer) { - DataT result; - access(registers, memory, instruction, pointer, result); - return result; - } - -template -template void DataPointerResolver::write( - RegistersT ®isters, - MemoryT &memory, - const Instruction &instruction, - DataPointer pointer, - DataT value) { - access(registers, memory, instruction, pointer, value); - } - -#define rw(v, r, is_write) \ - case Source::r: \ - using VType = typename std::remove_reference::type; \ - if constexpr (is_write) { \ - registers.template write(Source::r)>(v); \ - } else { \ - v = registers.template read(Source::r)>(); \ - } \ - break; - -#define ALLREGS(v, i) rw(v, eAX, i); rw(v, eCX, i); \ - rw(v, eDX, i); rw(v, eBX, i); \ - rw(v, eSPorAH, i); rw(v, eBPorCH, i); \ - rw(v, eSIorDH, i); rw(v, eDIorBH, i); \ - rw(v, ES, i); rw(v, CS, i); \ - rw(v, SS, i); rw(v, DS, i); \ - rw(v, FS, i); rw(v, GS, i); - -template -template -uint32_t DataPointerResolver::effective_address( - RegistersT ®isters, - const Instruction &instruction, - DataPointer pointer) { - using AddressT = typename Instruction::AddressT; - AddressT base = 0, index = 0; - - if constexpr (has_base) { - switch(pointer.base()) { - default: break; - ALLREGS(base, false); - } - } - - switch(pointer.index()) { - default: break; - ALLREGS(index, false); - } - - uint32_t address = index; - if constexpr (model >= Model::i80386) { - address <<= pointer.scale(); - } else { - assert(!pointer.scale()); - } - - // Always compute address as 32-bit. - // TODO: verify use of memory_mask around here. - // Also I think possibly an exception is supposed to be generated - // if the programmer is in 32-bit mode and has asked for 16-bit - // address computation but generated e.g. a 17-bit result. Look into - // that when working on execution. For now the goal is merely decoding - // and this code exists both to verify the presence of all necessary - // fields and to help to explore the best breakdown of storage - // within Instruction. - constexpr uint32_t memory_masks[] = {0x0000'ffff, 0xffff'ffff}; - const uint32_t memory_mask = memory_masks[int(instruction.address_size())]; - address = (address & memory_mask) + (base & memory_mask) + instruction.displacement(); - return address; - } - -template -template void DataPointerResolver::access( - RegistersT ®isters, - MemoryT &memory, - const Instruction &instruction, - DataPointer pointer, - DataT &value) { - const Source source = pointer.source(); - - switch(source) { - default: - if constexpr (!is_write) { - value = 0; - } - return; - - ALLREGS(value, is_write); - - case Source::DirectAddress: - if constexpr(is_write) { - memory.template write(instruction.data_segment(), instruction.displacement(), value); - } else { - value = memory.template read(instruction.data_segment(), instruction.displacement()); - } - break; - case Source::Immediate: - value = DataT(instruction.operand()); - break; - -#define indirect(has_base) { \ - const auto address = effective_address \ - (registers, instruction, pointer); \ - \ - if constexpr (is_write) { \ - memory.template write( \ - instruction.data_segment(), \ - address, \ - value \ - ); \ - } else { \ - value = memory.template read( \ - instruction.data_segment(), \ - address \ - ); \ - } \ -} - case Source::IndirectNoBase: - indirect(false); - break; - - case Source::Indirect: - indirect(true); - break; -#undef indirect - - } - } -#undef ALLREGS -#undef rw - -} -} - -#endif /* DataPointerResolver_hpp */ diff --git a/InstructionSets/x86/Decoder.cpp b/InstructionSets/x86/Decoder.cpp index d4db0b021..50b5c0d6b 100644 --- a/InstructionSets/x86/Decoder.cpp +++ b/InstructionSets/x86/Decoder.cpp @@ -15,7 +15,7 @@ using namespace InstructionSet::x86; template -std::pair::InstructionT> Decoder::decode(const uint8_t *source, size_t length) { +std::pair::InstructionT> Decoder::decode(const uint8_t *source, std::size_t length) { // Instruction length limits: // // 8086/80186: none* @@ -26,16 +26,20 @@ std::pair::InstructionT> Decoder::decode(con // be back to wherever it started, so it's safe to spit out a NOP and reset parsing // without any loss of context. This reduces the risk of the decoder tricking a caller into // an infinite loop. - constexpr int max_instruction_length = model >= Model::i80386 ? 15 : (model == Model::i80286 ? 10 : 65536); + static constexpr int max_instruction_length = model >= Model::i80386 ? 15 : (model == Model::i80286 ? 10 : 65536); const uint8_t *const end = source + std::min(length, size_t(max_instruction_length - consumed_)); // MARK: - Prefixes (if present) and the opcode. +/// Sets the operation and verifies that the current repetition, if any, is compatible, discarding it otherwise. +#define SetOperation(op) \ + operation_ = rep_operation(op, repetition_); + /// Helper macro for those that follow. #define SetOpSrcDestSize(op, src, dest, size) \ - operation_ = Operation::op; \ - source_ = Source::src; \ - destination_ = Source::dest; \ + SetOperation(Operation::op); \ + source_ = Source::src; \ + destination_ = Source::dest; \ operation_size_ = size /// Covers anything which is complete as soon as the opcode is encountered. @@ -53,19 +57,21 @@ std::pair::InstructionT> Decoder::decode(con /// Handles instructions of the form Ax, jjkk where the latter is implicitly an address. #define RegAddr(op, dest, op_size, addr_size) \ SetOpSrcDestSize(op, DirectAddress, dest, op_size); \ - operand_size_ = addr_size; \ - phase_ = Phase::DisplacementOrOperand + displacement_size_ = addr_size; \ + phase_ = Phase::DisplacementOrOperand; \ + sign_extend_displacement_ = false /// Handles instructions of the form jjkk, Ax where the former is implicitly an address. #define AddrReg(op, source, op_size, addr_size) \ SetOpSrcDestSize(op, source, DirectAddress, op_size); \ - operand_size_ = addr_size; \ + displacement_size_ = addr_size; \ destination_ = Source::DirectAddress; \ - phase_ = Phase::DisplacementOrOperand + phase_ = Phase::DisplacementOrOperand; \ + sign_extend_displacement_ = false /// Covers both `mem/reg, reg` and `reg, mem/reg`. #define MemRegReg(op, format, size) \ - operation_ = Operation::op; \ + SetOperation(Operation::op); \ phase_ = Phase::ModRegRM; \ modregrm_format_ = ModRegRMFormat::format; \ operand_size_ = DataSize::None; \ @@ -73,27 +79,28 @@ std::pair::InstructionT> Decoder::decode(con /// Handles JO, JNO, JB, etc — anything with only a displacement. #define Displacement(op, size) \ - operation_ = Operation::op; \ + SetOperation(Operation::op); \ phase_ = Phase::DisplacementOrOperand; \ - displacement_size_ = size + operation_size_= displacement_size_ = size /// Handles PUSH [immediate], etc — anything with only an immediate operand. #define Immediate(op, size) \ - operation_ = Operation::op; \ + SetOperation(Operation::op); \ source_ = Source::Immediate; \ phase_ = Phase::DisplacementOrOperand; \ operand_size_ = size /// Handles far CALL and far JMP — fixed four or six byte operand operations. -#define Far(op) \ - operation_ = Operation::op; \ - phase_ = Phase::DisplacementOrOperand; \ - operand_size_ = DataSize::Word; \ +#define Far(op) \ + SetOperation(Operation::op); \ + phase_ = Phase::DisplacementOrOperand; \ + operation_size_ = operand_size_ = DataSize::Word; \ + destination_ = Source::Immediate; \ displacement_size_ = data_size(default_address_size_) /// Handles ENTER — a fixed three-byte operation. #define Displacement16Operand8(op) \ - operation_ = Operation::op; \ + SetOperation(Operation::op); \ phase_ = Phase::DisplacementOrOperand; \ displacement_size_ = DataSize::Word; \ operand_size_ = DataSize::Byte @@ -141,8 +148,11 @@ std::pair::InstructionT> Decoder::decode(con // The 286 onwards have a further set of instructions // prefixed with $0f. case 0x0f: - RequiresMin(i80286); - phase_ = Phase::InstructionPageF; + if constexpr (model < Model::i80286) { + Complete(POP, None, CS, data_size_); + } else { + phase_ = Phase::InstructionPageF; + } break; PartialBlock(0x10, ADC); break; @@ -155,19 +165,19 @@ std::pair::InstructionT> Decoder::decode(con PartialBlock(0x20, AND); break; case 0x26: segment_override_ = Source::ES; break; - case 0x27: Complete(DAA, eAX, eAX, DataSize::Byte); break; + case 0x27: Complete(DAA, None, None, DataSize::Byte); break; PartialBlock(0x28, SUB); break; case 0x2e: segment_override_ = Source::CS; break; - case 0x2f: Complete(DAS, eAX, eAX, DataSize::Byte); break; + case 0x2f: Complete(DAS, None, None, DataSize::Byte); break; PartialBlock(0x30, XOR); break; case 0x36: segment_override_ = Source::SS; break; - case 0x37: Complete(AAA, eAX, eAX, DataSize::Word); break; + case 0x37: Complete(AAA, None, None, DataSize::Word); break; PartialBlock(0x38, CMP); break; case 0x3e: segment_override_ = Source::DS; break; - case 0x3f: Complete(AAS, eAX, eAX, DataSize::Word); break; + case 0x3f: Complete(AAS, None, None, DataSize::Word); break; #undef PartialBlock @@ -189,80 +199,132 @@ std::pair::InstructionT> Decoder::decode(con #undef RegisterBlock case 0x60: - RequiresMin(i80186); - Complete(PUSHA, None, None, data_size_); + if constexpr (model < Model::i80186) { + Displacement(JO, DataSize::Byte); + } else { + Complete(PUSHA, None, None, data_size_); + } break; case 0x61: - RequiresMin(i80186); - Complete(POPA, None, None, data_size_); + if constexpr (model < Model::i80186) { + Displacement(JNO, DataSize::Byte); + } else { + Complete(POPA, None, None, data_size_); + } break; case 0x62: - RequiresMin(i80186); - MemRegReg(BOUND, Reg_MemReg, data_size_); + if constexpr (model < Model::i80186) { + Displacement(JB, DataSize::Byte); + } else { + MemRegReg(BOUND, Reg_MemReg, data_size_); + } break; case 0x63: - RequiresMin(i80286); - MemRegReg(ARPL, MemReg_Reg, DataSize::Word); + if constexpr (model < Model::i80286) { + Displacement(JNB, DataSize::Byte); + } else { + MemRegReg(ARPL, MemReg_Reg, DataSize::Word); + } break; case 0x64: - RequiresMin(i80386); - segment_override_ = Source::FS; + if constexpr (model < Model::i80386) { + Displacement(JZ, DataSize::Byte); + } else { + RequiresMin(i80386); + segment_override_ = Source::FS; + } break; case 0x65: + if constexpr (model < Model::i80286) { + Displacement(JNZ, DataSize::Byte); + break; + } RequiresMin(i80386); segment_override_ = Source::GS; break; case 0x66: + if constexpr (model < Model::i80286) { + Displacement(JBE, DataSize::Byte); + break; + } RequiresMin(i80386); data_size_ = DataSize(int(default_data_size_) ^ int(DataSize::Word) ^ int(DataSize::DWord)); break; case 0x67: + if constexpr (model < Model::i80286) { + Displacement(JNBE, DataSize::Byte); + break; + } RequiresMin(i80386); address_size_ = AddressSize(int(default_address_size_) ^ int(AddressSize::b16) ^ int(AddressSize::b32)); break; case 0x68: - RequiresMin(i80286); - Immediate(PUSH, data_size_); - operation_size_ = data_size_; + if constexpr (model < Model::i80286) { + Displacement(JS, DataSize::Byte); + } else { + Immediate(PUSH, data_size_); + operation_size_ = data_size_; + } break; case 0x69: - RequiresMin(i80286); - MemRegReg(IMUL_3, Reg_MemReg, data_size_); - operand_size_ = data_size_; + if constexpr (model < Model::i80286) { + Displacement(JNS, DataSize::Byte); + } else { + MemRegReg(IMUL_3, Reg_MemReg, data_size_); + operand_size_ = data_size_; + } break; case 0x6a: - RequiresMin(i80286); - Immediate(PUSH, DataSize::Byte); + if constexpr (model < Model::i80286) { + Displacement(JP, DataSize::Byte); + } else { + Immediate(PUSH, DataSize::Byte); + } break; case 0x6b: - RequiresMin(i80286); - MemRegReg(IMUL_3, Reg_MemReg, data_size_); - operand_size_ = DataSize::Byte; - sign_extend_ = true; + if constexpr (model < Model::i80286) { + Displacement(JNP, DataSize::Byte); + } else { + MemRegReg(IMUL_3, Reg_MemReg, data_size_); + operand_size_ = DataSize::Byte; + sign_extend_operand_ = true; + } break; case 0x6c: // INSB - RequiresMin(i80186); - Complete(INS, None, None, DataSize::Byte); + if constexpr (model < Model::i80186) { + Displacement(JL, DataSize::Byte); + } else { + Complete(INS, None, None, DataSize::Byte); + } break; case 0x6d: // INSW/INSD - RequiresMin(i80186); - Complete(INS, None, None, data_size_); + if constexpr (model < Model::i80186) { + Displacement(JNL, DataSize::Byte); + } else { + Complete(INS, None, None, data_size_); + } break; case 0x6e: // OUTSB - RequiresMin(i80186); - Complete(OUTS, None, None, DataSize::Byte); + if constexpr (model < Model::i80186) { + Displacement(JLE, DataSize::Byte); + } else { + Complete(OUTS, None, None, DataSize::Byte); + } break; case 0x6f: // OUTSW/OUSD - RequiresMin(i80186); - Complete(OUTS, None, None, data_size_); + if constexpr (model < Model::i80186) { + Displacement(JNLE, DataSize::Byte); + } else { + Complete(OUTS, None, None, data_size_); + } break; case 0x70: Displacement(JO, DataSize::Byte); break; case 0x71: Displacement(JNO, DataSize::Byte); break; case 0x72: Displacement(JB, DataSize::Byte); break; case 0x73: Displacement(JNB, DataSize::Byte); break; - case 0x74: Displacement(JE, DataSize::Byte); break; - case 0x75: Displacement(JNE, DataSize::Byte); break; + case 0x74: Displacement(JZ, DataSize::Byte); break; + case 0x75: Displacement(JNZ, DataSize::Byte); break; case 0x76: Displacement(JBE, DataSize::Byte); break; case 0x77: Displacement(JNBE, DataSize::Byte); break; case 0x78: Displacement(JS, DataSize::Byte); break; @@ -292,7 +354,7 @@ std::pair::InstructionT> Decoder::decode(con case 0x8e: MemRegReg(MOV, Seg_MemReg, DataSize::Word); break; case 0x8f: MemRegReg(POP, MemRegSingleOperand, data_size_); break; - case 0x90: Complete(NOP, None, None, DataSize::None); break; // Or XCHG AX, AX? + case 0x90: Complete(NOP, None, None, DataSize::Byte); break; // Could be encoded as XCHG AX, AX if Operation space becomes limited. case 0x91: Complete(XCHG, eAX, eCX, data_size_); break; case 0x92: Complete(XCHG, eAX, eDX, data_size_); break; case 0x93: Complete(XCHG, eAX, eBX, data_size_); break; @@ -301,10 +363,10 @@ std::pair::InstructionT> Decoder::decode(con case 0x96: Complete(XCHG, eAX, eSI, data_size_); break; case 0x97: Complete(XCHG, eAX, eDI, data_size_); break; - case 0x98: Complete(CBW, eAX, AH, data_size_); break; - case 0x99: Complete(CWD, eAX, eDX, data_size_); break; + case 0x98: Complete(CBW, None, None, data_size_); break; + case 0x99: Complete(CWD, None, None, data_size_); break; case 0x9a: Far(CALLfar); break; - case 0x9b: Complete(WAIT, None, None, DataSize::None); break; + case 0x9b: Complete(WAIT, None, None, DataSize::Byte); break; case 0x9c: Complete(PUSHF, None, None, data_size_); break; case 0x9d: Complete(POPF, None, None, data_size_); break; case 0x9e: Complete(SAHF, None, None, DataSize::Byte); break; @@ -345,30 +407,48 @@ std::pair::InstructionT> Decoder::decode(con case 0xbe: RegData(MOV, eSI, data_size_); break; case 0xbf: RegData(MOV, eDI, data_size_); break; - case 0xc0: case 0xc1: - RequiresMin(i80186); - ShiftGroup(); - source_ = Source::Immediate; - operand_size_ = DataSize::Byte; + case 0xc0: + if constexpr (model >= Model::i80186) { + ShiftGroup(); + source_ = Source::Immediate; + operand_size_ = DataSize::Byte; + } else { + RegData(RETnear, None, data_size_); + } + break; + case 0xc1: + if constexpr (model >= Model::i80186) { + ShiftGroup(); + source_ = Source::Immediate; + operand_size_ = data_size_; + } else { + Complete(RETnear, None, None, DataSize::Byte); + } break; case 0xc2: RegData(RETnear, None, data_size_); break; - case 0xc3: Complete(RETnear, None, None, DataSize::None); break; + case 0xc3: Complete(RETnear, None, None, DataSize::Byte); break; case 0xc4: MemRegReg(LES, Reg_MemReg, data_size_); break; case 0xc5: MemRegReg(LDS, Reg_MemReg, data_size_); break; case 0xc6: MemRegReg(MOV, MemRegMOV, DataSize::Byte); break; case 0xc7: MemRegReg(MOV, MemRegMOV, data_size_); break; case 0xc8: - RequiresMin(i80186); - Displacement16Operand8(ENTER); + if constexpr (model >= Model::i80186) { + Displacement16Operand8(ENTER); + } else { + RegData(RETfar, None, data_size_); + } break; case 0xc9: - RequiresMin(i80186); - Complete(LEAVE, None, None, DataSize::None); + if constexpr (model >= Model::i80186) { + Complete(LEAVE, None, None, DataSize::Byte); + } else { + Complete(RETfar, None, None, DataSize::Word); + } break; case 0xca: RegData(RETfar, None, data_size_); break; - case 0xcb: Complete(RETfar, None, None, DataSize::DWord); break; + case 0xcb: Complete(RETfar, None, None, DataSize::Word); break; case 0xcc: // Encode INT3 as though it were INT with an @@ -377,13 +457,11 @@ std::pair::InstructionT> Decoder::decode(con operand_ = 3; break; case 0xcd: RegData(INT, None, DataSize::Byte); break; - case 0xce: Complete(INTO, None, None, DataSize::None); break; - case 0xcf: Complete(IRET, None, None, DataSize::None); break; + case 0xce: Complete(INTO, None, None, DataSize::Byte); break; + case 0xcf: Complete(IRET, None, None, DataSize::Byte); break; case 0xd0: case 0xd1: ShiftGroup(); - source_ = Source::Immediate; - operand_ = 1; break; case 0xd2: case 0xd3: ShiftGroup(); @@ -391,32 +469,32 @@ std::pair::InstructionT> Decoder::decode(con break; case 0xd4: RegData(AAM, eAX, DataSize::Byte); break; case 0xd5: RegData(AAD, eAX, DataSize::Byte); break; - // Unused: 0xd6. + case 0xd6: Complete(SALC, None, None, DataSize::Byte); break; case 0xd7: Complete(XLAT, None, None, DataSize::Byte); break; - case 0xd8: MemRegReg(ESC, MemReg_Reg, DataSize::None); break; - case 0xd9: MemRegReg(ESC, MemReg_Reg, DataSize::None); break; - case 0xda: MemRegReg(ESC, MemReg_Reg, DataSize::None); break; - case 0xdb: MemRegReg(ESC, MemReg_Reg, DataSize::None); break; - case 0xdc: MemRegReg(ESC, MemReg_Reg, DataSize::None); break; - case 0xdd: MemRegReg(ESC, MemReg_Reg, DataSize::None); break; - case 0xde: MemRegReg(ESC, MemReg_Reg, DataSize::None); break; - case 0xdf: MemRegReg(ESC, MemReg_Reg, DataSize::None); break; + case 0xd8: MemRegReg(ESC, Reg_MemReg, data_size_); break; + case 0xd9: MemRegReg(ESC, Reg_MemReg, data_size_); break; + case 0xda: MemRegReg(ESC, Reg_MemReg, data_size_); break; + case 0xdb: MemRegReg(ESC, Reg_MemReg, data_size_); break; + case 0xdc: MemRegReg(ESC, Reg_MemReg, data_size_); break; + case 0xdd: MemRegReg(ESC, Reg_MemReg, data_size_); break; + case 0xde: MemRegReg(ESC, Reg_MemReg, data_size_); break; + case 0xdf: MemRegReg(ESC, Reg_MemReg, data_size_); break; case 0xe0: Displacement(LOOPNE, DataSize::Byte); break; case 0xe1: Displacement(LOOPE, DataSize::Byte); break; case 0xe2: Displacement(LOOP, DataSize::Byte); break; - case 0xe3: Displacement(JPCX, DataSize::Byte); break; + case 0xe3: Displacement(JCXZ, DataSize::Byte); break; case 0xe4: RegAddr(IN, eAX, DataSize::Byte, DataSize::Byte); break; case 0xe5: RegAddr(IN, eAX, data_size_, DataSize::Byte); break; case 0xe6: AddrReg(OUT, eAX, DataSize::Byte, DataSize::Byte); break; case 0xe7: AddrReg(OUT, eAX, data_size_, DataSize::Byte); break; - case 0xe8: Displacement(CALLrel, data_size_); break; - case 0xe9: Displacement(JMPrel, data_size_); break; - case 0xea: Far(JMPfar); break; - case 0xeb: Displacement(JMPrel, DataSize::Byte); break; + case 0xe8: Displacement(CALLrel, data_size(address_size_)); break; + case 0xe9: Displacement(JMPrel, data_size(address_size_)); break; + case 0xea: Far(JMPfar); break; + case 0xeb: Displacement(JMPrel, DataSize::Byte); break; case 0xec: Complete(IN, eDX, eAX, DataSize::Byte); break; case 0xed: Complete(IN, eDX, eAX, data_size_); break; @@ -428,17 +506,17 @@ std::pair::InstructionT> Decoder::decode(con case 0xf2: repetition_ = Repetition::RepNE; break; case 0xf3: repetition_ = Repetition::RepE; break; - case 0xf4: Complete(HLT, None, None, DataSize::None); break; - case 0xf5: Complete(CMC, None, None, DataSize::None); break; + case 0xf4: Complete(HLT, None, None, DataSize::Byte); break; + case 0xf5: Complete(CMC, None, None, DataSize::Byte); break; case 0xf6: MemRegReg(Invalid, MemRegTEST_to_IDIV, DataSize::Byte); break; case 0xf7: MemRegReg(Invalid, MemRegTEST_to_IDIV, data_size_); break; - case 0xf8: Complete(CLC, None, None, DataSize::None); break; - case 0xf9: Complete(STC, None, None, DataSize::None); break; - case 0xfa: Complete(CLI, None, None, DataSize::None); break; - case 0xfb: Complete(STI, None, None, DataSize::None); break; - case 0xfc: Complete(CLD, None, None, DataSize::None); break; - case 0xfd: Complete(STD, None, None, DataSize::None); break; + case 0xf8: Complete(CLC, None, None, DataSize::Byte); break; + case 0xf9: Complete(STC, None, None, DataSize::Byte); break; + case 0xfa: Complete(CLI, None, None, DataSize::Byte); break; + case 0xfb: Complete(STI, None, None, DataSize::Byte); break; + case 0xfc: Complete(CLD, None, None, DataSize::Byte); break; + case 0xfd: Complete(STD, None, None, DataSize::Byte); break; case 0xfe: MemRegReg(Invalid, MemRegINC_DEC, DataSize::Byte); break; case 0xff: MemRegReg(Invalid, MemRegINC_to_PUSH, data_size_); break; @@ -447,145 +525,147 @@ std::pair::InstructionT> Decoder::decode(con // MARK: - Additional F page of instructions. - if(phase_ == Phase::InstructionPageF && source != end) { - // Update the instruction acquired. - const uint8_t instr = *source; - ++source; - ++consumed_; + if constexpr (model >= Model::i80286) { + if(phase_ == Phase::InstructionPageF && source != end) { + // Update the instruction acquired. + const uint8_t instr = *source; + ++source; + ++consumed_; - // NB: to reach here, the instruction set must be at least - // that of an 80286. - switch(instr) { - default: undefined(); + // NB: to reach here, the instruction set must be at least + // that of an 80286. + switch(instr) { + default: undefined(); - case 0x00: MemRegReg(Invalid, MemRegSLDT_to_VERW, data_size_); break; - case 0x01: MemRegReg(Invalid, MemRegSGDT_to_LMSW, data_size_); break; - case 0x02: MemRegReg(LAR, Reg_MemReg, data_size_); break; - case 0x03: MemRegReg(LSL, Reg_MemReg, data_size_); break; - case 0x05: - Requires(i80286); - Complete(LOADALL, None, None, DataSize::None); - break; - case 0x06: Complete(CLTS, None, None, DataSize::Byte); break; + case 0x00: MemRegReg(Invalid, MemRegSLDT_to_VERW, data_size_); break; + case 0x01: MemRegReg(Invalid, MemRegSGDT_to_LMSW, data_size_); break; + case 0x02: MemRegReg(LAR, Reg_MemReg, data_size_); break; + case 0x03: MemRegReg(LSL, Reg_MemReg, data_size_); break; + case 0x05: + Requires(i80286); + Complete(LOADALL, None, None, DataSize::Byte); + break; + case 0x06: Complete(CLTS, None, None, DataSize::Byte); break; - case 0x20: - RequiresMin(i80386); - MemRegReg(MOVfromCr, Reg_MemReg, DataSize::DWord); - break; - case 0x21: - RequiresMin(i80386); - MemRegReg(MOVfromDr, Reg_MemReg, DataSize::DWord); - break; - case 0x22: - RequiresMin(i80386); - MemRegReg(MOVtoCr, Reg_MemReg, DataSize::DWord); - break; - case 0x23: - RequiresMin(i80386); - MemRegReg(MOVtoDr, Reg_MemReg, DataSize::DWord); - break; - case 0x24: - RequiresMin(i80386); - MemRegReg(MOVfromTr, Reg_MemReg, DataSize::DWord); - break; - case 0x26: - RequiresMin(i80386); - MemRegReg(MOVtoTr, Reg_MemReg, DataSize::DWord); - break; + case 0x20: + RequiresMin(i80386); + MemRegReg(MOVfromCr, Reg_MemReg, DataSize::DWord); + break; + case 0x21: + RequiresMin(i80386); + MemRegReg(MOVfromDr, Reg_MemReg, DataSize::DWord); + break; + case 0x22: + RequiresMin(i80386); + MemRegReg(MOVtoCr, Reg_MemReg, DataSize::DWord); + break; + case 0x23: + RequiresMin(i80386); + MemRegReg(MOVtoDr, Reg_MemReg, DataSize::DWord); + break; + case 0x24: + RequiresMin(i80386); + MemRegReg(MOVfromTr, Reg_MemReg, DataSize::DWord); + break; + case 0x26: + RequiresMin(i80386); + MemRegReg(MOVtoTr, Reg_MemReg, DataSize::DWord); + break; - case 0x70: RequiresMin(i80386); Displacement(JO, data_size_); break; - case 0x71: RequiresMin(i80386); Displacement(JNO, data_size_); break; - case 0x72: RequiresMin(i80386); Displacement(JB, data_size_); break; - case 0x73: RequiresMin(i80386); Displacement(JNB, data_size_); break; - case 0x74: RequiresMin(i80386); Displacement(JE, data_size_); break; - case 0x75: RequiresMin(i80386); Displacement(JNE, data_size_); break; - case 0x76: RequiresMin(i80386); Displacement(JBE, data_size_); break; - case 0x77: RequiresMin(i80386); Displacement(JNBE, data_size_); break; - case 0x78: RequiresMin(i80386); Displacement(JS, data_size_); break; - case 0x79: RequiresMin(i80386); Displacement(JNS, data_size_); break; - case 0x7a: RequiresMin(i80386); Displacement(JP, data_size_); break; - case 0x7b: RequiresMin(i80386); Displacement(JNP, data_size_); break; - case 0x7c: RequiresMin(i80386); Displacement(JL, data_size_); break; - case 0x7d: RequiresMin(i80386); Displacement(JNL, data_size_); break; - case 0x7e: RequiresMin(i80386); Displacement(JLE, data_size_); break; - case 0x7f: RequiresMin(i80386); Displacement(JNLE, data_size_); break; + case 0x70: RequiresMin(i80386); Displacement(JO, data_size_); break; + case 0x71: RequiresMin(i80386); Displacement(JNO, data_size_); break; + case 0x72: RequiresMin(i80386); Displacement(JB, data_size_); break; + case 0x73: RequiresMin(i80386); Displacement(JNB, data_size_); break; + case 0x74: RequiresMin(i80386); Displacement(JZ, data_size_); break; + case 0x75: RequiresMin(i80386); Displacement(JNZ, data_size_); break; + case 0x76: RequiresMin(i80386); Displacement(JBE, data_size_); break; + case 0x77: RequiresMin(i80386); Displacement(JNBE, data_size_); break; + case 0x78: RequiresMin(i80386); Displacement(JS, data_size_); break; + case 0x79: RequiresMin(i80386); Displacement(JNS, data_size_); break; + case 0x7a: RequiresMin(i80386); Displacement(JP, data_size_); break; + case 0x7b: RequiresMin(i80386); Displacement(JNP, data_size_); break; + case 0x7c: RequiresMin(i80386); Displacement(JL, data_size_); break; + case 0x7d: RequiresMin(i80386); Displacement(JNL, data_size_); break; + case 0x7e: RequiresMin(i80386); Displacement(JLE, data_size_); break; + case 0x7f: RequiresMin(i80386); Displacement(JNLE, data_size_); break; #define Set(x) \ RequiresMin(i80386); \ MemRegReg(SET##x, MemRegSingleOperand, DataSize::Byte); - case 0x90: Set(O); break; - case 0x91: Set(NO); break; - case 0x92: Set(B); break; - case 0x93: Set(NB); break; - case 0x94: Set(Z); break; - case 0x95: Set(NZ); break; - case 0x96: Set(BE); break; - case 0x97: Set(NBE); break; - case 0x98: Set(S); break; - case 0x99: Set(NS); break; - case 0x9a: Set(P); break; - case 0x9b: Set(NP); break; - case 0x9c: Set(L); break; - case 0x9d: Set(NL); break; - case 0x9e: Set(LE); break; - case 0x9f: Set(NLE); break; + case 0x90: Set(O); break; + case 0x91: Set(NO); break; + case 0x92: Set(B); break; + case 0x93: Set(NB); break; + case 0x94: Set(Z); break; + case 0x95: Set(NZ); break; + case 0x96: Set(BE); break; + case 0x97: Set(NBE); break; + case 0x98: Set(S); break; + case 0x99: Set(NS); break; + case 0x9a: Set(P); break; + case 0x9b: Set(NP); break; + case 0x9c: Set(L); break; + case 0x9d: Set(NL); break; + case 0x9e: Set(LE); break; + case 0x9f: Set(NLE); break; #undef Set - case 0xa0: RequiresMin(i80386); Complete(PUSH, FS, None, data_size_); break; - case 0xa1: RequiresMin(i80386); Complete(POP, FS, None, data_size_); break; - case 0xa3: RequiresMin(i80386); MemRegReg(BT, MemReg_Reg, data_size_); break; - case 0xa4: - RequiresMin(i80386); - MemRegReg(SHLDimm, Reg_MemReg, data_size_); - operand_size_ = DataSize::Byte; - break; - case 0xa5: - RequiresMin(i80386); - MemRegReg(SHLDCL, MemReg_Reg, data_size_); - break; - case 0xa8: RequiresMin(i80386); Complete(PUSH, GS, None, data_size_); break; - case 0xa9: RequiresMin(i80386); Complete(POP, GS, None, data_size_); break; - case 0xab: RequiresMin(i80386); MemRegReg(BTS, MemReg_Reg, data_size_); break; - case 0xac: - RequiresMin(i80386); - MemRegReg(SHRDimm, Reg_MemReg, data_size_); - operand_size_ = DataSize::Byte; - break; - case 0xad: - RequiresMin(i80386); - MemRegReg(SHRDCL, MemReg_Reg, data_size_); - break; - case 0xaf: - RequiresMin(i80386); - MemRegReg(IMUL_2, Reg_MemReg, data_size_); - break; + case 0xa0: RequiresMin(i80386); Complete(PUSH, FS, None, data_size_); break; + case 0xa1: RequiresMin(i80386); Complete(POP, None, FS, data_size_); break; + case 0xa3: RequiresMin(i80386); MemRegReg(BT, MemReg_Reg, data_size_); break; + case 0xa4: + RequiresMin(i80386); + MemRegReg(SHLDimm, Reg_MemReg, data_size_); + operand_size_ = DataSize::Byte; + break; + case 0xa5: + RequiresMin(i80386); + MemRegReg(SHLDCL, MemReg_Reg, data_size_); + break; + case 0xa8: RequiresMin(i80386); Complete(PUSH, GS, None, data_size_); break; + case 0xa9: RequiresMin(i80386); Complete(POP, None, GS, data_size_); break; + case 0xab: RequiresMin(i80386); MemRegReg(BTS, MemReg_Reg, data_size_); break; + case 0xac: + RequiresMin(i80386); + MemRegReg(SHRDimm, Reg_MemReg, data_size_); + operand_size_ = DataSize::Byte; + break; + case 0xad: + RequiresMin(i80386); + MemRegReg(SHRDCL, MemReg_Reg, data_size_); + break; + case 0xaf: + RequiresMin(i80386); + MemRegReg(IMUL_2, Reg_MemReg, data_size_); + break; - case 0xb2: RequiresMin(i80386); MemRegReg(LSS, Reg_MemReg, data_size_); break; - case 0xb3: RequiresMin(i80386); MemRegReg(BTR, MemReg_Reg, data_size_); break; - case 0xb4: RequiresMin(i80386); MemRegReg(LFS, Reg_MemReg, data_size_); break; - case 0xb5: RequiresMin(i80386); MemRegReg(LGS, Reg_MemReg, data_size_); break; - case 0xb6: - RequiresMin(i80386); - MemRegReg(MOVZX, Reg_MemReg, DataSize::Byte); - break; - case 0xb7: - RequiresMin(i80386); - MemRegReg(MOVZX, Reg_MemReg, DataSize::Word); - break; - case 0xba: RequiresMin(i80386); MemRegReg(Invalid, MemRegBT_to_BTC, data_size_); break; - case 0xbb: RequiresMin(i80386); MemRegReg(BTC, MemReg_Reg, data_size_); break; - case 0xbc: RequiresMin(i80386); MemRegReg(BSF, MemReg_Reg, data_size_); break; - case 0xbd: RequiresMin(i80386); MemRegReg(BSR, MemReg_Reg, data_size_); break; - case 0xbe: - RequiresMin(i80386); - MemRegReg(MOVSX, Reg_MemReg, DataSize::Byte); - break; - case 0xbf: - RequiresMin(i80386); - MemRegReg(MOVSX, Reg_MemReg, DataSize::Word); - break; + case 0xb2: RequiresMin(i80386); MemRegReg(LSS, Reg_MemReg, data_size_); break; + case 0xb3: RequiresMin(i80386); MemRegReg(BTR, MemReg_Reg, data_size_); break; + case 0xb4: RequiresMin(i80386); MemRegReg(LFS, Reg_MemReg, data_size_); break; + case 0xb5: RequiresMin(i80386); MemRegReg(LGS, Reg_MemReg, data_size_); break; + case 0xb6: + RequiresMin(i80386); + MemRegReg(MOVZX, Reg_MemReg, DataSize::Byte); + break; + case 0xb7: + RequiresMin(i80386); + MemRegReg(MOVZX, Reg_MemReg, DataSize::Word); + break; + case 0xba: RequiresMin(i80386); MemRegReg(Invalid, MemRegBT_to_BTC, data_size_); break; + case 0xbb: RequiresMin(i80386); MemRegReg(BTC, MemReg_Reg, data_size_); break; + case 0xbc: RequiresMin(i80386); MemRegReg(BSF, MemReg_Reg, data_size_); break; + case 0xbd: RequiresMin(i80386); MemRegReg(BSR, MemReg_Reg, data_size_); break; + case 0xbe: + RequiresMin(i80386); + MemRegReg(MOVSX, Reg_MemReg, DataSize::Byte); + break; + case 0xbf: + RequiresMin(i80386); + MemRegReg(MOVSX, Reg_MemReg, DataSize::Word); + break; + } } } @@ -640,6 +720,10 @@ std::pair::InstructionT> Decoder::decode(con operation_ == Operation::LFS) { undefined(); } + } else if(rm == 6 && mod == 0) { + // There's no BP direct; BP with ostensibly no offset means 'direct address' mode. + displacement_size_ = data_size(address_size_); + memreg = Source::DirectAddress; } else { const DataSize sizes[] = { DataSize::None, @@ -647,29 +731,33 @@ std::pair::InstructionT> Decoder::decode(con data_size(address_size_) }; displacement_size_ = sizes[mod]; - memreg = Source::Indirect; - if(address_size_ == AddressSize::b32) { + if(is_32bit(model) && address_size_ == AddressSize::b32) { // 32-bit decoding: the range of potential indirections is expanded, // and may segue into obtaining a SIB. sib_ = ScaleIndexBase(0, Source::None, reg_table[rm]); expects_sib = rm == 4; // Indirect via eSP isn't directly supported; it's the // escape indicator for reading a SIB. + memreg = Source::Indirect; } else { // Classic 16-bit decoding: mode picks a displacement size, // and a few fixed index+base pairs are defined. + // + // A base of eAX is meaningless, with the source type being the indicator + // that it should be ignored. ScaleIndexBase can't store a base of Source::None. constexpr ScaleIndexBase rm_table[8] = { - ScaleIndexBase(0, Source::eBX, Source::eSI), - ScaleIndexBase(0, Source::eBX, Source::eDI), - ScaleIndexBase(0, Source::eBP, Source::eSI), - ScaleIndexBase(0, Source::eBP, Source::eDI), - ScaleIndexBase(0, Source::None, Source::eSI), - ScaleIndexBase(0, Source::None, Source::eDI), + ScaleIndexBase(0, Source::eSI, Source::eBX), + ScaleIndexBase(0, Source::eDI, Source::eBX), + ScaleIndexBase(0, Source::eSI, Source::eBP), + ScaleIndexBase(0, Source::eDI, Source::eBP), + ScaleIndexBase(0, Source::eSI, Source::eAX), + ScaleIndexBase(0, Source::eDI, Source::eAX), ScaleIndexBase(0, Source::None, Source::eBP), - ScaleIndexBase(0, Source::None, Source::eBX), + ScaleIndexBase(0, Source::eBX, Source::eAX), }; sib_ = rm_table[rm]; + memreg = (rm >= 4 && rm != 6) ? Source::IndirectNoBase : Source::Indirect; } } @@ -686,58 +774,86 @@ std::pair::InstructionT> Decoder::decode(con } break; case ModRegRMFormat::MemRegTEST_to_IDIV: - source_ = destination_ = memreg; + source_ = memreg; switch(reg) { - default: undefined(); + default: + // case 1 is treated as another form of TEST on the 8086. + // (and, I guess, the 80186?) + if constexpr (model >= Model::i80286) { + undefined(); + } + [[fallthrough]]; - case 0: operation_ = Operation::TEST; break; - case 2: operation_ = Operation::NOT; break; - case 3: operation_ = Operation::NEG; break; - case 4: operation_ = Operation::MUL; break; - case 5: operation_ = Operation::IMUL_1; break; - case 6: operation_ = Operation::DIV; break; - case 7: operation_ = Operation::IDIV; break; + case 0: + destination_ = memreg; + source_ = Source::Immediate; + operand_size_ = operation_size_; + SetOperation(Operation::TEST); + break; + case 2: SetOperation(Operation::NOT); break; + case 3: SetOperation(Operation::NEG); break; + case 4: SetOperation(Operation::MUL); break; + case 5: SetOperation(Operation::IMUL_1); break; + case 6: SetOperation(Operation::DIV); break; + case 7: SetOperation(Operation::IDIV); break; } break; case ModRegRMFormat::Seg_MemReg: - case ModRegRMFormat::MemReg_Seg: + case ModRegRMFormat::MemReg_Seg: { + // On the 8086, only two bits of reg are used. + const int masked_reg = model >= Model::i80286 ? reg : reg & 3; + // The 16-bit chips have four segment registers; // the 80386 onwards has six. - if(!is_32bit(model) && reg > 3) { - undefined(); - } else if(reg > 5) { - undefined(); + if constexpr (is_32bit(model)) { + if(masked_reg > 5) { + undefined(); + } + } else { + if(masked_reg > 3) { + undefined(); + } } if(modregrm_format_ == ModRegRMFormat::Seg_MemReg) { source_ = memreg; - destination_ = seg_table[reg]; + destination_ = seg_table[masked_reg]; // 80286 and later disallow MOV to CS. if(model >= Model::i80286 && destination_ == Source::CS) { undefined(); } } else { - source_ = seg_table[reg]; + source_ = seg_table[masked_reg]; destination_ = memreg; } - break; + } break; case ModRegRMFormat::MemRegROL_to_SAR: destination_ = memreg; switch(reg) { - default: undefined(); + default: + if constexpr (model == Model::i8086) { + if(source_ == Source::eCX) { + SetOperation(Operation::SETMOC); + } else { + SetOperation(Operation::SETMO); + } + } else { + undefined(); + } + break; - case 0: operation_ = Operation::ROL; break; - case 1: operation_ = Operation::ROR; break; - case 2: operation_ = Operation::RCL; break; - case 3: operation_ = Operation::RCR; break; - case 4: operation_ = Operation::SAL; break; - case 5: operation_ = Operation::SHR; break; - case 7: operation_ = Operation::SAR; break; + case 0: SetOperation(Operation::ROL); break; + case 1: SetOperation(Operation::ROR); break; + case 2: SetOperation(Operation::RCL); break; + case 3: SetOperation(Operation::RCR); break; + case 4: SetOperation(Operation::SAL); break; + case 5: SetOperation(Operation::SHR); break; + case 7: SetOperation(Operation::SAR); break; } break; @@ -745,10 +861,10 @@ std::pair::InstructionT> Decoder::decode(con source_ = destination_ = memreg; switch(reg) { - default: undefined(); + default: undefined(); - case 0: operation_ = Operation::INC; break; - case 1: operation_ = Operation::DEC; break; + case 0: SetOperation(Operation::INC); break; + case 1: SetOperation(Operation::DEC); break; } break; @@ -756,15 +872,21 @@ std::pair::InstructionT> Decoder::decode(con source_ = destination_ = memreg; switch(reg) { - default: undefined(); + default: + // case 7 is treated as another form of PUSH on the 8086. + // (and, I guess, the 80186?) + if constexpr (model >= Model::i80286) { + undefined(); + } + [[fallthrough]]; + case 6: SetOperation(Operation::PUSH); break; - case 0: operation_ = Operation::INC; break; - case 1: operation_ = Operation::DEC; break; - case 2: operation_ = Operation::CALLabs; break; - case 3: operation_ = Operation::CALLfar; break; - case 4: operation_ = Operation::JMPabs; break; - case 5: operation_ = Operation::JMPfar; break; - case 6: operation_ = Operation::PUSH; break; + case 0: SetOperation(Operation::INC); break; + case 1: SetOperation(Operation::DEC); break; + case 2: SetOperation(Operation::CALLabs); break; + case 3: SetOperation(Operation::CALLfar); break; + case 4: SetOperation(Operation::JMPabs); break; + case 5: SetOperation(Operation::JMPfar); break; } break; @@ -787,17 +909,17 @@ std::pair::InstructionT> Decoder::decode(con source_ = Source::Immediate; destination_ = memreg; operand_size_ = (modregrm_format_ == ModRegRMFormat::MemRegADD_to_CMP_SignExtend) ? DataSize::Byte : operation_size_; - sign_extend_ = true; // Will be effective only if modregrm_format_ == ModRegRMFormat::MemRegADD_to_CMP_SignExtend. + sign_extend_operand_ = true; // Will be effective only if modregrm_format_ == ModRegRMFormat::MemRegADD_to_CMP_SignExtend. switch(reg) { - default: operation_ = Operation::ADD; break; - case 1: operation_ = Operation::OR; break; - case 2: operation_ = Operation::ADC; break; - case 3: operation_ = Operation::SBB; break; - case 4: operation_ = Operation::AND; break; - case 5: operation_ = Operation::SUB; break; - case 6: operation_ = Operation::XOR; break; - case 7: operation_ = Operation::CMP; break; + default: SetOperation(Operation::ADD); break; + case 1: SetOperation(Operation::OR); break; + case 2: SetOperation(Operation::ADC); break; + case 3: SetOperation(Operation::SBB); break; + case 4: SetOperation(Operation::AND); break; + case 5: SetOperation(Operation::SUB); break; + case 6: SetOperation(Operation::XOR); break; + case 7: SetOperation(Operation::CMP); break; } break; @@ -807,12 +929,12 @@ std::pair::InstructionT> Decoder::decode(con switch(reg) { default: undefined(); - case 0: operation_ = Operation::SLDT; break; - case 1: operation_ = Operation::STR; break; - case 2: operation_ = Operation::LLDT; break; - case 3: operation_ = Operation::LTR; break; - case 4: operation_ = Operation::VERR; break; - case 5: operation_ = Operation::VERW; break; + case 0: SetOperation(Operation::SLDT); break; + case 1: SetOperation(Operation::STR); break; + case 2: SetOperation(Operation::LLDT); break; + case 3: SetOperation(Operation::LTR); break; + case 4: SetOperation(Operation::VERR); break; + case 5: SetOperation(Operation::VERW); break; } break; @@ -822,12 +944,12 @@ std::pair::InstructionT> Decoder::decode(con switch(reg) { default: undefined(); - case 0: operation_ = Operation::SGDT; break; - case 1: operation_ = Operation::SIDT; break; - case 2: operation_ = Operation::LGDT; break; - case 3: operation_ = Operation::LIDT; break; - case 4: operation_ = Operation::SMSW; break; - case 6: operation_ = Operation::LMSW; break; + case 0: SetOperation(Operation::SGDT); break; + case 1: SetOperation(Operation::SIDT); break; + case 2: SetOperation(Operation::LGDT); break; + case 3: SetOperation(Operation::LIDT); break; + case 4: SetOperation(Operation::SMSW); break; + case 6: SetOperation(Operation::LMSW); break; } break; @@ -839,10 +961,10 @@ std::pair::InstructionT> Decoder::decode(con switch(reg) { default: undefined(); - case 4: operation_ = Operation::BT; break; - case 5: operation_ = Operation::BTS; break; - case 6: operation_ = Operation::BTR; break; - case 7: operation_ = Operation::BTC; break; + case 4: SetOperation(Operation::BT); break; + case 5: SetOperation(Operation::BTS); break; + case 6: SetOperation(Operation::BTR); break; + case 7: SetOperation(Operation::BTC); break; } break; @@ -857,21 +979,24 @@ std::pair::InstructionT> Decoder::decode(con } #undef undefined +#undef SetOperation // MARK: - ScaleIndexBase - if(phase_ == Phase::ScaleIndexBase && source != end) { - sib_ = *source; - ++source; - ++consumed_; + if constexpr (is_32bit(model)) { + if(phase_ == Phase::ScaleIndexBase && source != end) { + sib_ = *source; + ++source; + ++consumed_; - // Potentially record the lack of a base. - if(displacement_size_ == DataSize::None && (uint8_t(sib_)&7) == 5) { - source_ = (source_ == Source::Indirect) ? Source::IndirectNoBase : source_; - destination_ = (destination_ == Source::Indirect) ? Source::IndirectNoBase : destination_; + // Potentially record the lack of a base. + if(displacement_size_ == DataSize::None && (uint8_t(sib_)&7) == 5) { + source_ = (source_ == Source::Indirect) ? Source::IndirectNoBase : source_; + destination_ = (destination_ == Source::Indirect) ? Source::IndirectNoBase : destination_; + } + + phase_ = (displacement_size_ != DataSize::None || operand_size_ != DataSize::None) ? Phase::DisplacementOrOperand : Phase::ReadyToPost; } - - phase_ = (displacement_size_ != DataSize::None || operand_size_ != DataSize::None) ? Phase::DisplacementOrOperand : Phase::ReadyToPost; } // MARK: - Displacement and operand. @@ -894,16 +1019,25 @@ std::pair::InstructionT> Decoder::decode(con if(bytes_to_consume == outstanding_bytes) { phase_ = Phase::ReadyToPost; - switch(displacement_size_) { - case DataSize::None: displacement_ = 0; break; - case DataSize::Byte: displacement_ = int8_t(inward_data_); break; - case DataSize::Word: displacement_ = int16_t(inward_data_); break; - case DataSize::DWord: displacement_ = int32_t(inward_data_); break; + if(!sign_extend_displacement_) { + switch(displacement_size_) { + case DataSize::None: displacement_ = 0; break; + case DataSize::Byte: displacement_ = uint8_t(inward_data_); break; + case DataSize::Word: displacement_ = uint16_t(inward_data_); break; + case DataSize::DWord: displacement_ = int32_t(inward_data_); break; + } + } else { + switch(displacement_size_) { + case DataSize::None: displacement_ = 0; break; + case DataSize::Byte: displacement_ = int8_t(inward_data_); break; + case DataSize::Word: displacement_ = int16_t(inward_data_); break; + case DataSize::DWord: displacement_ = int32_t(inward_data_); break; + } } inward_data_ >>= bit_size(displacement_size_); // Use inequality of sizes as a test for necessary sign extension. - if(operand_size_ == data_size_ || !sign_extend_) { + if(operand_size_ == data_size_ || !sign_extend_operand_) { operand_ = decltype(operand_)(inward_data_); } else { switch(operand_size_) { @@ -922,6 +1056,18 @@ std::pair::InstructionT> Decoder::decode(con // MARK: - Check for completion. if(phase_ == Phase::ReadyToPost) { + // TODO: map to #UD where applicable; build LOCK into the Operation type, buying an extra bit for the operation? + // + // As of the P6 Intel stipulates that: + // + // "The LOCK prefix can be prepended only to the following instructions and to those forms of the instructions + // that use a memory operand: ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, + // XADD, and XCHG." + // + // ... and the #UD exception will be raised if LOCK is encountered elsewhere. So adding 17 additional + // operations would unlock an extra bit of storage for a net gain of 239 extra operation types and thereby + // alleviating any concerns over whether there'll be space to handle MMX, floating point extensions, etc. + const auto result = std::make_pair( consumed_, InstructionT( @@ -932,11 +1078,9 @@ std::pair::InstructionT> Decoder::decode(con lock_, address_size_, segment_override_, - repetition_, - DataSize(operation_size_), + operation_size_, static_cast(displacement_), - static_cast(operand_), - consumed_ + static_cast(operand_) ) ); reset_parsing(); @@ -947,7 +1091,7 @@ std::pair::InstructionT> Decoder::decode(con if(consumed_ == max_instruction_length) { std::pair result; if(max_instruction_length == 65536) { - result = std::make_pair(consumed_, InstructionT(Operation::NOP, consumed_)); + result = std::make_pair(consumed_, InstructionT(Operation::NOP)); } else { result = std::make_pair(consumed_, InstructionT()); } @@ -979,3 +1123,7 @@ template class InstructionSet::x86::Decoder; template class InstructionSet::x86::Decoder; template class InstructionSet::x86::Decoder; template class InstructionSet::x86::Decoder; + +std::pair> Decoder8086::decode(const uint8_t *source, std::size_t length) { + return decoder.decode(source, length); +} diff --git a/InstructionSets/x86/Decoder.hpp b/InstructionSets/x86/Decoder.hpp index 02af8bfba..ee3c7e736 100644 --- a/InstructionSets/x86/Decoder.hpp +++ b/InstructionSets/x86/Decoder.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef InstructionSets_x86_Decoder_hpp -#define InstructionSets_x86_Decoder_hpp +#pragma once #include "Instruction.hpp" #include "Model.hpp" @@ -15,8 +14,7 @@ #include #include -namespace InstructionSet { -namespace x86 { +namespace InstructionSet::x86 { /*! Implements Intel x86 instruction decoding. @@ -39,13 +37,12 @@ template class Decoder { supplied cannot form a valid instruction. @discussion although instructions also contain an indicator of their length, on chips prior - to the 80286 there is no limit to instruction length and that could in theory overflow the available - storage, which can describe instructions only up to 1kb in size. + to the 80286 there is no limit to potential instruction length. The 80286 and 80386 have instruction length limits of 10 and 15 bytes respectively, so cannot overflow the field. */ - std::pair decode(const uint8_t *source, size_t length); + std::pair decode(const uint8_t *source, std::size_t length); /*! Enables or disables 32-bit protected mode. Meaningful only if the @c Model supports it. @@ -194,7 +191,9 @@ template class Decoder { DataSize operand_size_ = DataSize::None; // i.e. size of in-stream operand, if any. DataSize operation_size_ = DataSize::None; // i.e. size of data manipulated by the operation. - bool sign_extend_ = false; // If set then sign extend the operand up to the operation size; + bool sign_extend_displacement_ = true; // If set then sign extend any displacement up to the address + // size; otherwise it'll be zero-padded. + bool sign_extend_operand_ = false; // If set then sign extend the operand up to the operation size; // otherwise it'll be zero-padded. // Prefix capture fields. @@ -223,11 +222,22 @@ template class Decoder { sib_ = ScaleIndexBase(); next_inward_data_shift_ = 0; inward_data_ = 0; - sign_extend_ = false; + sign_extend_operand_ = false; + sign_extend_displacement_ = true; } }; -} -} +// This is a temporary measure; for reasons as-yet unknown, GCC isn't picking up the +// explicit instantiations of the template above at link time, even though is is +// unambiguously building and linking in Decoder.cpp. +// +// So here's a thin non-templated shim to unblock initial PC Compatible development. +class Decoder8086 { + public: + std::pair> decode(const uint8_t *source, std::size_t length); -#endif /* InstructionSets_x86_Decoder_hpp */ + private: + Decoder decoder; +}; + +} diff --git a/InstructionSets/x86/Documentation/80386 opcode map.html b/InstructionSets/x86/Documentation/80386 opcode map.html index 3bab74c42..09e6330e5 100644 --- a/InstructionSets/x86/Documentation/80386 opcode map.html +++ b/InstructionSets/x86/Documentation/80386 opcode map.html @@ -224,7 +224,7 @@ AND SEG =ES - POP ES + DAA SUB SEG =CS DAS diff --git a/InstructionSets/x86/Flags.hpp b/InstructionSets/x86/Flags.hpp new file mode 100644 index 000000000..8fd2f16ca --- /dev/null +++ b/InstructionSets/x86/Flags.hpp @@ -0,0 +1,230 @@ +// +// Flags.hpp +// Clock Signal +// +// Created by Thomas Harte on 05/10/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "../../Numeric/Carry.hpp" + +namespace InstructionSet::x86 { + +namespace FlagValue { + +// +// Standard flags. +// + +static constexpr uint32_t Carry = 1 << 0; +static constexpr uint32_t Parity = 1 << 2; +static constexpr uint32_t AuxiliaryCarry = 1 << 4; +static constexpr uint32_t Zero = 1 << 6; +static constexpr uint32_t Sign = 1 << 7; +static constexpr uint32_t Trap = 1 << 8; +static constexpr uint32_t Interrupt = 1 << 9; +static constexpr uint32_t Direction = 1 << 10; +static constexpr uint32_t Overflow = 1 << 11; + +// +// 80286+ additions. +// + +static constexpr uint32_t IOPrivilege = (1 << 12) | (1 << 13); +static constexpr uint32_t NestedTask = 1 << 14; + +// +// 16-bit protected mode flags. +// + +static constexpr uint32_t ProtectionEnable = 1 << 16; +static constexpr uint32_t MonitorProcessorExtension = 1 << 17; +static constexpr uint32_t ProcessorExtensionExtension = 1 << 18; +static constexpr uint32_t TaskSwitch = 1 << 19; + +// +// 32-bit protected mode flags. +// + +static constexpr uint32_t Resume = 1 << 16; +static constexpr uint32_t VirtualMode = 1 << 17; + +} + +enum class Flag { + Carry, + AuxiliaryCarry, + Sign, + Overflow, + Trap, + Interrupt, + Direction, + Zero, + ParityOdd +}; + +enum class Condition { + Overflow, + Below, + Zero, + BelowOrEqual, + Sign, + ParityOdd, + Less, + LessOrEqual +}; + +class Flags { + public: + using FlagT = uint32_t; + + // Flag getters. + template bool flag() const { + switch(flag_v) { + case Flag::Carry: return carry_; + case Flag::AuxiliaryCarry: return auxiliary_carry_; + case Flag::Sign: return sign_; + case Flag::Overflow: return overflow_; + case Flag::Trap: return trap_; + case Flag::Interrupt: return interrupt_; + case Flag::Direction: return direction_ < 0; + case Flag::Zero: return !zero_; + case Flag::ParityOdd: return not_parity_bit(); + } + } + + // Condition evaluation. + template bool condition() const { + switch(test) { + case Condition::Overflow: return flag(); + case Condition::Below: return flag(); + case Condition::Zero: return flag(); + case Condition::BelowOrEqual: return flag() || flag(); + case Condition::Sign: return flag(); + case Condition::ParityOdd: return flag(); + case Condition::Less: return flag() != flag(); + case Condition::LessOrEqual: return flag() || flag() != flag(); + } + } + + // Convenience setters. + + /// Sets all of @c flags as a function of @c value: + /// • Flag::Zero: sets the zero flag if @c value is zero; + /// • Flag::Sign: sets the sign flag if the top bit of @c value is one; + /// • Flag::ParityOdd: sets parity based on the low 8 bits of @c value; + /// • Flag::Carry: sets carry if @c value is non-zero; + /// • Flag::AuxiliaryCarry: sets auxiliary carry if @c value is non-zero; + /// • Flag::Overflow: sets overflow if @c value is non-zero; + /// • Flag::Interrupt: sets interrupt if @c value is non-zero; + /// • Flag::Trap: sets interrupt if @c value is non-zero; + /// • Flag::Direction: sets direction if @c value is non-zero. + template void set_from(IntT value) { + for(const auto flag: {flags...}) { + switch(flag) { + default: break; + case Flag::Zero: zero_ = value; break; + case Flag::Sign: sign_ = value & Numeric::top_bit(); break; + case Flag::ParityOdd: parity_ = value; break; + case Flag::Carry: carry_ = value; break; + case Flag::AuxiliaryCarry: auxiliary_carry_ = value; break; + case Flag::Overflow: overflow_ = value; break; + case Flag::Interrupt: interrupt_ = value; break; + case Flag::Trap: trap_ = value; break; + case Flag::Direction: direction_ = value ? -1 : 1; break; + } + } + } + template void set_from(FlagT value) { + set_from(value); + } + + template IntT carry_bit() const { return carry_ ? 1 : 0; } + bool not_parity_bit() const { + // x86 parity always considers the lowest 8-bits only. + auto result = static_cast(parity_); + result ^= result >> 4; + result ^= result >> 2; + result ^= result >> 1; + return result & 1; + } + + template IntT direction() const { return static_cast(direction_); } + + // Complete value get and set. + void set(uint16_t value) { + set_from(value & FlagValue::Carry); + set_from(value & FlagValue::AuxiliaryCarry); + set_from(value & FlagValue::Overflow); + set_from(value & FlagValue::Trap); + set_from(value & FlagValue::Interrupt); + set_from(value & FlagValue::Direction); + + set_from(uint8_t(value)); + + set_from((~value) & FlagValue::Zero); + set_from((~value) & FlagValue::Parity); + } + + uint16_t get() const { + return + 0xf002 | + + (flag() ? FlagValue::Carry : 0) | + (flag() ? FlagValue::AuxiliaryCarry : 0) | + (flag() ? FlagValue::Sign : 0) | + (flag() ? FlagValue::Overflow : 0) | + (flag() ? FlagValue::Trap : 0) | + (flag() ? FlagValue::Interrupt : 0) | + (flag() ? FlagValue::Direction : 0) | + (flag() ? FlagValue::Zero : 0) | + + (flag() ? 0 : FlagValue::Parity); + } + + std::string to_string() const { + std::string result; + + if(flag()) result += "O"; else result += "-"; + if(flag()) result += "D"; else result += "-"; + if(flag()) result += "I"; else result += "-"; + if(flag()) result += "T"; else result += "-"; + if(flag()) result += "S"; else result += "-"; + if(flag()) result += "Z"; else result += "-"; + result += "-"; + if(flag()) result += "A"; else result += "-"; + result += "-"; + if(!flag()) result += "P"; else result += "-"; + result += "-"; + if(flag()) result += "C"; else result += "-"; + + return result; + } + + bool operator ==(const Flags &rhs) const { + return get() == rhs.get(); + } + + private: + // Non-zero => set; zero => unset. + uint32_t carry_; + uint32_t auxiliary_carry_; + uint32_t sign_; + uint32_t overflow_; + uint32_t trap_; + uint32_t interrupt_; + + // +1 = direction flag not set; + // -1 = direction flag set. + int32_t direction_; + + // Zero => set; non-zero => unset. + uint32_t zero_; + + // Odd number of bits => set; even => unset. + uint32_t parity_; +}; + +} diff --git a/InstructionSets/x86/Implementation/Arithmetic.hpp b/InstructionSets/x86/Implementation/Arithmetic.hpp new file mode 100644 index 000000000..1066de82d --- /dev/null +++ b/InstructionSets/x86/Implementation/Arithmetic.hpp @@ -0,0 +1,362 @@ +// +// Arithmetic.hpp +// Clock Signal +// +// Created by Thomas Harte on 08/11/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "../AccessType.hpp" +#include "../Interrupts.hpp" +#include "../Perform.hpp" + +#include "../../../Numeric/Carry.hpp" + +namespace InstructionSet::x86::Primitive { + +template +void add( + modify_t destination, + read_t source, + ContextT &context +) { + /* + DEST ← DEST + SRC [+ CF]; + */ + /* + The OF, SF, ZF, AF, CF, and PF flags are set according to the result. + */ + const IntT result = destination + source + (with_carry ? context.flags.template carry_bit() : 0); + + context.flags.template set_from( + Numeric::carried_out() - 1>(destination, source, result)); + context.flags.template set_from( + Numeric::carried_in<4>(destination, source, result)); + context.flags.template set_from( + Numeric::overflow(destination, source, result)); + + context.flags.template set_from(result); + + destination = result; +} + +template +void sub( + access_t destination, + read_t source, + ContextT &context +) { + /* + DEST ← DEST - (SRC [+ CF]); + */ + /* + The OF, SF, ZF, AF, CF, and PF flags are set according to the result. + */ + const IntT result = destination - source - (with_borrow ? context.flags.template carry_bit() : 0); + + context.flags.template set_from( + Numeric::carried_out() - 1>(destination, source, result)); + context.flags.template set_from( + Numeric::carried_in<4>(destination, source, result)); + context.flags.template set_from( + Numeric::overflow(destination, source, result)); + + context.flags.template set_from(result); + + if constexpr (is_writeable(destination_type)) { + destination = result; + } +} + +template +void test( + read_t destination, + read_t source, + ContextT &context +) { + /* + TEMP ← SRC1 AND SRC2; + SF ← MSB(TEMP); + IF TEMP = 0 + THEN ZF ← 0; + ELSE ZF ← 1; + FI: + PF ← BitwiseXNOR(TEMP[0:7]); + CF ← 0; + OF ← 0; + */ + /* + The OF and CF flags are cleared to 0. + The SF, ZF, and PF flags are set according to the result (see the “Operation” section above). + The state of the AF flag is undefined. + */ + const IntT result = destination & source; + + context.flags.template set_from(0); + context.flags.template set_from(result); +} + +template +void mul( + modify_t destination_high, + modify_t destination_low, + read_t source, + ContextT &context +) { + /* + IF byte operation + THEN + AX ← AL * SRC + ELSE (* word or doubleword operation *) + IF OperandSize = 16 THEN + DX:AX ← AX * SRC + ELSE (* OperandSize = 32 *) + EDX:EAX ← EAX * SRC + FI; + */ + /* + The OF and CF flags are cleared to 0 if the upper half of the result is 0; + otherwise, they are set to 1. The SF, ZF, AF, and PF flags are undefined. + */ + destination_high = (destination_low * source) >> (8 * sizeof(IntT)); + destination_low *= source; + context.flags.template set_from(destination_high); +} + +template +void imul( + modify_t destination_high, + modify_t destination_low, + read_t source, + ContextT &context +) { + /* + (as modified by https://www.felixcloutier.com/x86/daa ...) + + IF (OperandSize = 8) + THEN + AX ← AL ∗ SRC (* signed multiplication *) + IF (AX = SignExtend(AL)) + THEN CF = 0; OF = 0; + ELSE CF = 1; OF = 1; + FI; + ELSE IF OperandSize = 16 + THEN + DX:AX ← AX ∗ SRC (* signed multiplication *) + IF (DX:AX = SignExtend(AX)) + THEN CF = 0; OF = 0; + ELSE CF = 1; OF = 1; + FI; + ELSE (* OperandSize = 32 *) + EDX:EAX ← EAX ∗ SRC (* signed multiplication *) + IF (EDX:EAX = SignExtend(EAX)) + THEN CF = 0; OF = 0; + ELSE CF = 1; OF = 1; + FI; + FI; + */ + using sIntT = typename std::make_signed::type; + destination_high = IntT((sIntT(destination_low) * sIntT(source)) >> (8 * sizeof(IntT))); + destination_low = IntT(sIntT(destination_low) * sIntT(source)); + + const auto sign_extension = (destination_low & Numeric::top_bit()) ? IntT(~0) : 0; + context.flags.template set_from(destination_high != sign_extension); +} + +template +void div( + modify_t destination_high, + modify_t destination_low, + read_t source, + ContextT &context +) { + /* + IF SRC = 0 + THEN #DE; (* divide error *) + FI; + IF OperandSize = 8 (* word/byte operation *) + THEN + temp ← AX / SRC; + IF temp > FFH + THEN #DE; (* divide error *) ; + ELSE + AL ← temp; + AH ← AX MOD SRC; + FI; + ELSE + IF OperandSize = 16 (* doubleword/word operation *) + THEN + temp ← DX:AX / SRC; + IF temp > FFFFH + THEN #DE; (* divide error *) ; + ELSE + AX ← temp; + DX ← DX:AX MOD SRC; + FI; + ELSE (* quadword/doubleword operation *) + temp ← EDX:EAX / SRC; + IF temp > FFFFFFFFH + THEN #DE; (* divide error *) ; + ELSE + EAX ← temp; + EDX ← EDX:EAX MOD SRC; + FI; + FI; + FI; + */ + /* + The CF, OF, SF, ZF, AF, and PF flags are undefined. + */ + if(!source) { + interrupt(Interrupt::DivideError, context); + return; + } + + // TEMPORARY HACK. Will not work with DWords. + const uint32_t dividend = uint32_t((destination_high << (8 * sizeof(IntT))) + destination_low); + const auto result = dividend / source; + if(IntT(result) != result) { + interrupt(Interrupt::DivideError, context); + return; + } + + destination_low = IntT(result); + destination_high = dividend % source; +} + +template +void idiv( + modify_t destination_high, + modify_t destination_low, + read_t source, + ContextT &context +) { + /* + IF SRC = 0 + THEN #DE; (* divide error *) + FI; + IF OperandSize = 8 (* word/byte operation *) + THEN + temp ← AX / SRC; (* signed division *) + IF (temp > 7FH) OR (temp < 80H) (* if a positive result is greater than 7FH or a negative result is less than 80H *) + THEN #DE; (* divide error *) ; + ELSE + AL ← temp; + AH ← AX MOD SRC; + FI; + ELSE + IF OperandSize = 16 (* doubleword/word operation *) + THEN + temp ← DX:AX / SRC; (* signed division *) + IF (temp > 7FFFH) OR (temp < 8000H) (* if a positive result is greater than 7FFFH or a negative result is less than 8000H *) + THEN #DE; (* divide error *) ; + ELSE + AX ← temp; + DX ← DX:AX MOD SRC; + FI; + ELSE (* quadword/doubleword operation *) + temp ← EDX:EAX / SRC; (* signed division *) + IF (temp > 7FFFFFFFH) OR (temp < 80000000H) (* if a positive result is greater than 7FFFFFFFH or a negative result is less than 80000000H *) + THEN #DE; (* divide error *) ; + ELSE + EAX ← temp; + EDX ← EDX:EAX MOD SRC; + FI; + FI; + FI; + */ + /* + The CF, OF, SF, ZF, AF, and PF flags are undefined. + */ + if(!source) { + interrupt(Interrupt::DivideError, context); + return; + } + + // TEMPORARY HACK. Will not work with DWords. + using sIntT = typename std::make_signed::type; + const int32_t dividend = (sIntT(destination_high) << (8 * sizeof(IntT))) + destination_low; + auto result = dividend / sIntT(source); + + // An 8086 quirk: rep IDIV performs an IDIV that switches the sign on its result, + // due to reuse of an internal flag. + if constexpr (invert) { + result = -result; + } + + if(sIntT(result) != result) { + interrupt(Interrupt::DivideError, context); + return; + } + + destination_low = IntT(result); + destination_high = IntT(dividend % sIntT(source)); +} + +template +void inc( + modify_t destination, + ContextT &context +) { + /* + DEST ← DEST + 1; + */ + /* + The CF flag is not affected. + The OF, SF, ZF, AF, and PF flags are set according to the result. + */ + ++destination; + + context.flags.template set_from(destination == Numeric::top_bit()); + context.flags.template set_from(((destination - 1) ^ destination) & 0x10); + context.flags.template set_from(destination); +} + +template +void dec( + modify_t destination, + ContextT &context +) { + /* + DEST ← DEST - 1; + */ + /* + The CF flag is not affected. + The OF, SF, ZF, AF, and PF flags are set according to the result. + */ + context.flags.template set_from(destination == Numeric::top_bit()); + + --destination; + + context.flags.template set_from(destination); + context.flags.template set_from(((destination + 1) ^ destination) & 0x10); +} + +template +void neg( + modify_t destination, + ContextT &context +) { + /* + IF DEST = 0 + THEN CF ← 0 + ELSE CF ← 1; + FI; + DEST ← –(DEST) + */ + /* + The CF flag cleared to 0 if the source operand is 0; otherwise it is set to 1. + The OF, SF, ZF, AF, and PF flags are set according to the result. + */ + context.flags.template set_from(Numeric::carried_in<4>(IntT(0), destination, IntT(-destination))); + + destination = -destination; + + context.flags.template set_from(destination); + context.flags.template set_from(destination == Numeric::top_bit()); + context.flags.template set_from(destination); +} + +} diff --git a/InstructionSets/x86/Implementation/BCD.hpp b/InstructionSets/x86/Implementation/BCD.hpp new file mode 100644 index 000000000..21212a8db --- /dev/null +++ b/InstructionSets/x86/Implementation/BCD.hpp @@ -0,0 +1,113 @@ +// +// BCD.hpp +// Clock Signal +// +// Created by Thomas Harte on 08/11/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "../AccessType.hpp" + +#include "../../../Numeric/RegisterSizes.hpp" + +namespace InstructionSet::x86::Primitive { + +/// If @c add is @c true, performs an AAA; otherwise perfoms an AAS. +template +void aaas( + CPU::RegisterPair16 &ax, + ContextT &context +) { + if((ax.halves.low & 0x0f) > 9 || context.flags.template flag()) { + if constexpr (add) { + ax.halves.low += 6; + ++ax.halves.high; + } else { + ax.halves.low -= 6; + --ax.halves.high; + } + context.flags.template set_from(1); + } else { + context.flags.template set_from(0); + } + ax.halves.low &= 0x0f; +} + +template +void aad( + CPU::RegisterPair16 &ax, + uint8_t imm, + ContextT &context +) { + /* + tempAL ← AL; + tempAH ← AH; + AL ← (tempAL + (tempAH * imm8)) AND FFH; (* imm8 is set to 0AH for the AAD mnemonic *) + AH ← 0 + */ + /* + The SF, ZF, and PF flags are set according to the result; + the OF, AF, and CF flags are undefined. + */ + ax.halves.low = ax.halves.low + (ax.halves.high * imm); + ax.halves.high = 0; + context.flags.template set_from(ax.halves.low); +} + +template +void aam( + CPU::RegisterPair16 &ax, + uint8_t imm, + ContextT &context +) { + /* + tempAL ← AL; + AH ← tempAL / imm8; (* imm8 is set to 0AH for the AAD mnemonic *) + AL ← tempAL MOD imm8; + */ + /* + The SF, ZF, and PF flags are set according to the result. + The OF, AF, and CF flags are undefined. + */ + /* + If ... an immediate value of 0 is used, it will cause a #DE (divide error) exception. + */ + if(!imm) { + interrupt(Interrupt::DivideError, context); + return; + } + + ax.halves.high = ax.halves.low / imm; + ax.halves.low = ax.halves.low % imm; + context.flags.template set_from(ax.halves.low); +} + +/// If @c add is @c true, performs a DAA; otherwise perfoms a DAS. +template +void daas( + uint8_t &al, + ContextT &context +) { + bool top_exceeded_threshold; + if constexpr (ContextT::model == Model::i8086) { + top_exceeded_threshold = al > (context.flags.template flag() ? 0x9f : 0x99); + } else { + top_exceeded_threshold = al > 0x99; + } + + if((al & 0x0f) > 0x09 || context.flags.template flag()) { + if constexpr (add) al += 0x06; else al -= 0x06; + context.flags.template set_from(1); + } + + if(top_exceeded_threshold || context.flags.template flag()) { + if constexpr (add) al += 0x60; else al -= 0x60; + context.flags.template set_from(1); + } + + context.flags.template set_from(al); +} + +} diff --git a/InstructionSets/x86/Implementation/FlowControl.hpp b/InstructionSets/x86/Implementation/FlowControl.hpp new file mode 100644 index 000000000..65af8a425 --- /dev/null +++ b/InstructionSets/x86/Implementation/FlowControl.hpp @@ -0,0 +1,245 @@ +// +// FlowControl.hpp +// Clock Signal +// +// Created by Thomas Harte on 08/11/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "Resolver.hpp" +#include "Stack.hpp" +#include "../AccessType.hpp" + +#include + +namespace InstructionSet::x86::Primitive { + +template +void jump( + bool condition, + IntT displacement, + ContextT &context +) { + /* + IF condition + THEN + EIP ← EIP + SignExtend(DEST); + IF OperandSize = 16 + THEN + EIP ← EIP AND 0000FFFFH; + FI; + FI; + */ + + // TODO: proper behaviour in 32-bit. + if(condition) { + context.flow_controller.template jump(uint16_t(context.registers.ip() + displacement)); + } +} + +template +void loop( + modify_t counter, + OffsetT displacement, + ContextT &context +) { + --counter; + if(counter) { + context.flow_controller.template jump(context.registers.ip() + displacement); + } +} + +template +void loope( + modify_t counter, + OffsetT displacement, + ContextT &context +) { + --counter; + if(counter && context.flags.template flag()) { + context.flow_controller.template jump(context.registers.ip() + displacement); + } +} + +template +void loopne( + modify_t counter, + OffsetT displacement, + ContextT &context +) { + --counter; + if(counter && !context.flags.template flag()) { + context.flow_controller.template jump(context.registers.ip() + displacement); + } +} + +template +void call_relative( + typename std::make_signed::type offset, + ContextT &context +) { + if constexpr (std::is_same_v) { + push(context.registers.ip(), context); + context.flow_controller.template jump(AddressT(context.registers.ip() + offset)); + } else { + assert(false); + } +} + +template +void call_absolute( + read_t target, + ContextT &context +) { + push(context.registers.ip(), context); + context.flow_controller.template jump(AddressT(target)); +} + +template +void jump_absolute( + read_t target, + ContextT &context +) { + context.flow_controller.template jump(target); +} + +template +void call_far( + InstructionT &instruction, + ContextT &context +) { + // TODO: eliminate 16-bit assumption below. + const Source source_segment = instruction.data_segment(); + context.memory.preauthorise_stack_write(sizeof(uint16_t) * 2); + + uint16_t source_address; + const auto pointer = instruction.destination(); + switch(pointer.source()) { + default: + case Source::Immediate: + push(context.registers.cs(), context); + push(context.registers.ip(), context); + context.flow_controller.template jump(instruction.segment(), instruction.offset()); + return; + + case Source::Indirect: + source_address = uint16_t(address(instruction, pointer, context)); + break; + case Source::IndirectNoBase: + source_address = uint16_t(address(instruction, pointer, context)); + break; + case Source::DirectAddress: + source_address = uint16_t(address(instruction, pointer, context)); + break; + } + + context.memory.preauthorise_read(source_segment, source_address, sizeof(uint16_t) * 2); + const uint16_t offset = context.memory.template access(source_segment, source_address); + source_address += 2; + const uint16_t segment = context.memory.template access(source_segment, source_address); + + // At least on an 8086, the stack writes occur after the target address read. + push(context.registers.cs(), context); + push(context.registers.ip(), context); + + context.flow_controller.template jump(segment, offset); +} + +template +void jump_far( + InstructionT &instruction, + ContextT &context +) { + // TODO: eliminate 16-bit assumption below. + uint16_t source_address = 0; + const auto pointer = instruction.destination(); + switch(pointer.source()) { + default: + case Source::Immediate: context.flow_controller.template jump(instruction.segment(), instruction.offset()); return; + + case Source::Indirect: + source_address = uint16_t(address(instruction, pointer, context)); + break; + case Source::IndirectNoBase: + source_address = uint16_t(address(instruction, pointer, context)); + break; + case Source::DirectAddress: + source_address = uint16_t(address(instruction, pointer, context)); + break; + } + + const Source source_segment = instruction.data_segment(); + context.memory.preauthorise_read(source_segment, source_address, sizeof(uint16_t) * 2); + + const uint16_t offset = context.memory.template access(source_segment, source_address); + source_address += 2; + const uint16_t segment = context.memory.template access(source_segment, source_address); + context.flow_controller.template jump(segment, offset); +} + +template +void iret( + ContextT &context +) { + // TODO: all modes other than 16-bit real mode. + context.memory.preauthorise_stack_read(sizeof(uint16_t) * 3); + const auto ip = pop(context); + const auto cs = pop(context); + context.flags.set(pop(context)); + context.flow_controller.template jump(cs, ip); +} + +template +void ret_near( + InstructionT instruction, + ContextT &context +) { + const auto ip = pop(context); + context.registers.sp() += instruction.operand(); + context.flow_controller.template jump(ip); +} + +template +void ret_far( + InstructionT instruction, + ContextT &context +) { + context.memory.preauthorise_stack_read(sizeof(uint16_t) * 2); + const auto ip = pop(context); + const auto cs = pop(context); + context.registers.sp() += instruction.operand(); + context.flow_controller.template jump(cs, ip); +} + +template +void into( + ContextT &context +) { + if(context.flags.template flag()) { + interrupt(Interrupt::Overflow, context); + } +} + +template +void bound( + const InstructionT &instruction, + read_t destination, + read_t source, + ContextT &context +) { + using sIntT = typename std::make_signed::type; + + const auto source_segment = instruction.data_segment(); + context.memory.preauthorise_read(source_segment, source, 2*sizeof(IntT)); + const sIntT lower_bound = sIntT(context.memory.template access(source_segment, source)); + source += 2; + const sIntT upper_bound = sIntT(context.memory.template access(source_segment, source)); + + if(sIntT(destination) < lower_bound || sIntT(destination) > upper_bound) { + interrupt(Interrupt::BoundRangeExceeded, context); + } +} + +} diff --git a/InstructionSets/x86/Implementation/InOut.hpp b/InstructionSets/x86/Implementation/InOut.hpp new file mode 100644 index 000000000..3f1b9d98f --- /dev/null +++ b/InstructionSets/x86/Implementation/InOut.hpp @@ -0,0 +1,33 @@ +// +// InOut.hpp +// Clock Signal +// +// Created by Thomas Harte on 08/11/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "../AccessType.hpp" + +namespace InstructionSet::x86::Primitive { + +template +void out( + uint16_t port, + read_t value, + ContextT &context +) { + context.io.template out(port, value); +} + +template +void in( + uint16_t port, + write_t value, + ContextT &context +) { + value = context.io.template in(port); +} + +} diff --git a/InstructionSets/x86/Implementation/LoadStore.hpp b/InstructionSets/x86/Implementation/LoadStore.hpp new file mode 100644 index 000000000..e03c1e4ea --- /dev/null +++ b/InstructionSets/x86/Implementation/LoadStore.hpp @@ -0,0 +1,80 @@ +// +// LoadStore.hpp +// Clock Signal +// +// Created by Thomas Harte on 08/11/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "../AccessType.hpp" + +#include + +namespace InstructionSet::x86::Primitive { + +template +void xchg( + modify_t destination, + modify_t source +) { + /* + TEMP ← DEST + DEST ← SRC + SRC ← TEMP + */ + std::swap(destination, source); +} + +template +void ld( + const InstructionT &instruction, + write_t destination, + ContextT &context +) { + const auto pointer = instruction.source(); + uint16_t source_address = uint16_t(address(instruction, pointer, context)); + const Source source_segment = instruction.data_segment(); + + context.memory.preauthorise_read(source_segment, source_address, 4); + destination = context.memory.template access(source_segment, source_address); + source_address += 2; + switch(selector) { + case Source::DS: context.registers.ds() = context.memory.template access(source_segment, source_address); break; + case Source::ES: context.registers.es() = context.memory.template access(source_segment, source_address); break; + } +} + +template +void lea( + const InstructionT &instruction, + write_t destination, + ContextT &context +) { + // TODO: address size. + destination = IntT(address(instruction, instruction.source(), context)); +} + +template +void xlat( + const InstructionT &instruction, + ContextT &context +) { + AddressT address; + if constexpr (std::is_same_v) { + address = context.registers.bx() + context.registers.al(); + } + + context.registers.al() = context.memory.template access(instruction.data_segment(), address); +} + +template +void mov( + write_t destination, + read_t source +) { + destination = source; +} + +} diff --git a/InstructionSets/x86/Implementation/Logical.hpp b/InstructionSets/x86/Implementation/Logical.hpp new file mode 100644 index 000000000..186b3963d --- /dev/null +++ b/InstructionSets/x86/Implementation/Logical.hpp @@ -0,0 +1,143 @@ +// +// Logical.hpp +// Clock Signal +// +// Created by Thomas Harte on 08/11/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "../AccessType.hpp" + +namespace InstructionSet::x86::Primitive { + +template +void and_( + modify_t destination, + read_t source, + ContextT &context +) { + /* + DEST ← DEST AND SRC; + */ + /* + The OF and CF flags are cleared; the SF, ZF, and PF flags are set according to the result. + The state of the AF flag is undefined. + */ + destination &= source; + + context.flags.template set_from(0); + context.flags.template set_from(destination); +} + +template +void or_( + modify_t destination, + read_t source, + ContextT &context +) { + /* + DEST ← DEST OR SRC; + */ + /* + The OF and CF flags are cleared; the SF, ZF, and PF flags are set according to the result. + The state of the AF flag is undefined. + */ + destination |= source; + + context.flags.template set_from(0); + context.flags.template set_from(destination); +} + +template +void xor_( + modify_t destination, + read_t source, + ContextT &context +) { + /* + DEST ← DEST XOR SRC; + */ + /* + The OF and CF flags are cleared; the SF, ZF, and PF flags are set according to the result. + The state of the AF flag is undefined. + */ + destination ^= source; + + context.flags.template set_from(0); + context.flags.template set_from(destination); +} + +template +void not_( + modify_t destination +) { + /* + DEST ← NOT DEST; + */ + /* + Flags affected: none. + */ + destination = ~destination; +} + +template +void cbw( + IntT &ax +) { + constexpr IntT test_bit = 1 << (sizeof(IntT) * 4 - 1); + constexpr IntT low_half = (1 << (sizeof(IntT) * 4)) - 1; + + if(ax & test_bit) { + ax |= ~low_half; + } else { + ax &= low_half; + } +} + +template +void cwd( + IntT &dx, + IntT ax +) { + dx = ax & Numeric::top_bit() ? IntT(~0) : IntT(0); +} + +// TODO: changes to the interrupt flag do quite a lot more in protected mode. +template +void clc(ContextT &context) { context.flags.template set_from(0); } +template +void cld(ContextT &context) { context.flags.template set_from(0); } +template +void cli(ContextT &context) { context.flags.template set_from(0); } +template +void stc(ContextT &context) { context.flags.template set_from(1); } +template +void std(ContextT &context) { context.flags.template set_from(1); } +template +void sti(ContextT &context) { context.flags.template set_from(1); } +template +void cmc(ContextT &context) { + context.flags.template set_from(!context.flags.template flag()); +} + +template +void salc( + uint8_t &al, + ContextT &context +) { + al = context.flags.template flag() ? 0xff : 0x00; +} + +template +void setmo( + write_t destination, + ContextT &context +) { + const auto result = destination = IntT(~0); + context.flags.template set_from(0); + context.flags.template set_from(result); +} + +} diff --git a/InstructionSets/x86/Implementation/PerformImplementation.hpp b/InstructionSets/x86/Implementation/PerformImplementation.hpp new file mode 100644 index 000000000..7d9e1c9de --- /dev/null +++ b/InstructionSets/x86/Implementation/PerformImplementation.hpp @@ -0,0 +1,533 @@ +// +// +// PerformImplementation.hpp +// Clock Signal +// +// Created by Thomas Harte on 05/10/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "Arithmetic.hpp" +#include "BCD.hpp" +#include "FlowControl.hpp" +#include "InOut.hpp" +#include "LoadStore.hpp" +#include "Logical.hpp" +#include "Repetition.hpp" +#include "Resolver.hpp" +#include "ShiftRoll.hpp" +#include "Stack.hpp" + +#include "../Interrupts.hpp" +#include "../AccessType.hpp" + +// +// Comments throughout headers above come from the 1997 edition of the +// Intel Architecture Software Developer’s Manual; that year all such +// definitions still fitted within a single volume, Volume 2. +// +// Order Number 243191; e.g. https://www.ardent-tool.com/CPU/docs/Intel/IA/243191-002.pdf +// + +namespace InstructionSet::x86 { + +template < + DataSize data_size, + AddressSize address_size, + typename InstructionT, + typename ContextT +> void perform( + const InstructionT &instruction, + ContextT &context +) { + using IntT = typename DataSizeType::type; + using AddressT = typename AddressSizeType::type; + + // Establish source() and destination() shorthands to fetch data if necessary. + // + // C++17, which this project targets at the time of writing, does not provide templatised lambdas. + // So the following division is in part a necessity. + // + // (though GCC offers C++20 syntax as an extension, and Clang seems to follow along, so maybe I'm overthinking) + IntT immediate; + const auto source_r = [&]() -> read_t { + return resolve( + instruction, + instruction.source().source(), + instruction.source(), + context, + nullptr, + &immediate); + }; + const auto source_rmw = [&]() -> modify_t { + return resolve( + instruction, + instruction.source().source(), + instruction.source(), + context, + nullptr, + &immediate); + }; + const auto destination_r = [&]() -> read_t { + return resolve( + instruction, + instruction.destination().source(), + instruction.destination(), + context, + nullptr, + &immediate); + }; + const auto destination_w = [&]() -> write_t { + return resolve( + instruction, + instruction.destination().source(), + instruction.destination(), + context, + nullptr, + &immediate); + }; + const auto destination_rmw = [&]() -> modify_t { + return resolve( + instruction, + instruction.destination().source(), + instruction.destination(), + context, + nullptr, + &immediate); + }; + + // Performs a displacement jump only if @c condition is true. + const auto jcc = [&](bool condition) { + Primitive::jump( + condition, + instruction.displacement(), + context); + }; + + const auto shift_count = [&]() -> uint8_t { + static constexpr uint8_t mask = (ContextT::model != Model::i8086) ? 0x1f : 0xff; + switch(instruction.source().source()) { + case Source::None: return 1; + case Source::Immediate: return uint8_t(instruction.operand()) & mask; + default: return context.registers.cl() & mask; + } + }; + + // Some instructions use a pair of registers as an extended accumulator — DX:AX or EDX:EAX. + // The two following return the high and low parts of that pair; they also work in Byte mode to return AH:AL, + // i.e. AX split into high and low parts. + const auto pair_high = [&]() -> IntT& { + if constexpr (data_size == DataSize::Byte) return context.registers.ah(); + else if constexpr (data_size == DataSize::Word) return context.registers.dx(); + else if constexpr (data_size == DataSize::DWord) return context.registers.edx(); + }; + const auto pair_low = [&]() -> IntT& { + if constexpr (data_size == DataSize::Byte) return context.registers.al(); + else if constexpr (data_size == DataSize::Word) return context.registers.ax(); + else if constexpr (data_size == DataSize::DWord) return context.registers.eax(); + }; + + // For the string operations, evaluate to either SI and DI or ESI and EDI, depending on the address size. + const auto eSI = [&]() -> AddressT& { + if constexpr (std::is_same_v) { + return context.registers.si(); + } else { + return context.registers.esi(); + } + }; + const auto eDI = [&]() -> AddressT& { + if constexpr (std::is_same_v) { + return context.registers.di(); + } else { + return context.registers.edi(); + } + }; + + // For counts, provide either eCX or CX depending on address size. + const auto eCX = [&]() -> AddressT& { + if constexpr (std::is_same_v) { + return context.registers.cx(); + } else { + return context.registers.ecx(); + } + }; + + // Gets the port for an IN or OUT; these are always 16-bit. + const auto port = [&](Source source) -> uint16_t { + switch(source) { + case Source::DirectAddress: return instruction.offset(); + default: return context.registers.dx(); + } + }; + + // Guide to the below: + // + // * use hard-coded register names where appropriate, otherwise use the source_X() and destination_X() lambdas; + // * return directly if there is definitely no possible write back to RAM; + // * break if there's a chance of writeback. + switch(instruction.operation()) { + default: + assert(false); + + case Operation::Invalid: + // TODO: throw on higher-order processors. + case Operation::ESC: + case Operation::NOP: return; + + case Operation::AAM: Primitive::aam(context.registers.axp(), uint8_t(instruction.operand()), context); return; + case Operation::AAD: Primitive::aad(context.registers.axp(), uint8_t(instruction.operand()), context); return; + case Operation::AAA: Primitive::aaas(context.registers.axp(), context); return; + case Operation::AAS: Primitive::aaas(context.registers.axp(), context); return; + case Operation::DAA: Primitive::daas(context.registers.al(), context); return; + case Operation::DAS: Primitive::daas(context.registers.al(), context); return; + + case Operation::CBW: Primitive::cbw(pair_low()); return; + case Operation::CWD: Primitive::cwd(pair_high(), pair_low()); return; + + case Operation::HLT: context.flow_controller.halt(); return; + case Operation::WAIT: context.flow_controller.wait(); return; + + case Operation::ADC: + Primitive::add(destination_rmw(), source_r(), context); + break; + case Operation::ADD: + Primitive::add(destination_rmw(), source_r(), context); + break; + case Operation::SBB: + Primitive::sub(destination_rmw(), source_r(), context); + break; + case Operation::SUB: + Primitive::sub(destination_rmw(), source_r(), context); + break; + case Operation::CMP: + Primitive::sub(destination_r(), source_r(), context); + return; + case Operation::TEST: + Primitive::test(destination_r(), source_r(), context); + return; + + case Operation::MUL: Primitive::mul(pair_high(), pair_low(), source_r(), context); return; + case Operation::IMUL_1: Primitive::imul(pair_high(), pair_low(), source_r(), context); return; + case Operation::DIV: Primitive::div(pair_high(), pair_low(), source_r(), context); return; + case Operation::IDIV: Primitive::idiv(pair_high(), pair_low(), source_r(), context); return; + case Operation::IDIV_REP: + if constexpr (ContextT::model == Model::i8086) { + Primitive::idiv(pair_high(), pair_low(), source_r(), context); + break; + } else { + static_assert(int(Operation::IDIV_REP) == int(Operation::LEAVE)); + if constexpr (std::is_same_v || std::is_same_v) { + Primitive::leave(); + } + } + return; + + case Operation::INC: Primitive::inc(destination_rmw(), context); break; + case Operation::DEC: Primitive::dec(destination_rmw(), context); break; + + case Operation::AND: Primitive::and_(destination_rmw(), source_r(), context); break; + case Operation::OR: Primitive::or_(destination_rmw(), source_r(), context); break; + case Operation::XOR: Primitive::xor_(destination_rmw(), source_r(), context); break; + case Operation::NEG: Primitive::neg(source_rmw(), context); break; // TODO: should be a destination. + case Operation::NOT: Primitive::not_(source_rmw()); break; // TODO: should be a destination. + + case Operation::CALLrel: + Primitive::call_relative(instruction.displacement(), context); + return; + case Operation::CALLabs: Primitive::call_absolute(destination_r(), context); return; + case Operation::CALLfar: Primitive::call_far(instruction, context); return; + + case Operation::JMPrel: jcc(true); return; + case Operation::JMPabs: Primitive::jump_absolute(destination_r(), context); return; + case Operation::JMPfar: Primitive::jump_far(instruction, context); return; + + case Operation::JCXZ: jcc(!eCX()); return; + case Operation::LOOP: Primitive::loop(eCX(), instruction.offset(), context); return; + case Operation::LOOPE: Primitive::loope(eCX(), instruction.offset(), context); return; + case Operation::LOOPNE: Primitive::loopne(eCX(), instruction.offset(), context); return; + + case Operation::IRET: Primitive::iret(context); return; + case Operation::RETnear: Primitive::ret_near(instruction, context); return; + case Operation::RETfar: Primitive::ret_far(instruction, context); return; + + case Operation::INT: interrupt(instruction.operand(), context); return; + case Operation::INTO: Primitive::into(context); return; + + case Operation::SAHF: Primitive::sahf(context.registers.ah(), context); return; + case Operation::LAHF: Primitive::lahf(context.registers.ah(), context); return; + + case Operation::LDS: + if constexpr (data_size == DataSize::Word) { + Primitive::ld(instruction, destination_w(), context); + context.segments.did_update(Source::DS); + } + return; + case Operation::LES: + if constexpr (data_size == DataSize::Word) { + Primitive::ld(instruction, destination_w(), context); + context.segments.did_update(Source::ES); + } + return; + + case Operation::LEA: Primitive::lea(instruction, destination_w(), context); return; + case Operation::MOV: + Primitive::mov(destination_w(), source_r()); + if constexpr (std::is_same_v) { + context.segments.did_update(instruction.destination().source()); + } + break; + + case Operation::JO: jcc(context.flags.template condition()); return; + case Operation::JNO: jcc(!context.flags.template condition()); return; + case Operation::JB: jcc(context.flags.template condition()); return; + case Operation::JNB: jcc(!context.flags.template condition()); return; + case Operation::JZ: jcc(context.flags.template condition()); return; + case Operation::JNZ: jcc(!context.flags.template condition()); return; + case Operation::JBE: jcc(context.flags.template condition()); return; + case Operation::JNBE: jcc(!context.flags.template condition()); return; + case Operation::JS: jcc(context.flags.template condition()); return; + case Operation::JNS: jcc(!context.flags.template condition()); return; + case Operation::JP: jcc(!context.flags.template condition()); return; + case Operation::JNP: jcc(context.flags.template condition()); return; + case Operation::JL: jcc(context.flags.template condition()); return; + case Operation::JNL: jcc(!context.flags.template condition()); return; + case Operation::JLE: jcc(context.flags.template condition()); return; + case Operation::JNLE: jcc(!context.flags.template condition()); return; + + case Operation::RCL: Primitive::rcl(destination_rmw(), shift_count(), context); break; + case Operation::RCR: Primitive::rcr(destination_rmw(), shift_count(), context); break; + case Operation::ROL: Primitive::rol(destination_rmw(), shift_count(), context); break; + case Operation::ROR: Primitive::ror(destination_rmw(), shift_count(), context); break; + case Operation::SAL: Primitive::sal(destination_rmw(), shift_count(), context); break; + case Operation::SAR: Primitive::sar(destination_rmw(), shift_count(), context); break; + case Operation::SHR: Primitive::shr(destination_rmw(), shift_count(), context); break; + + case Operation::CLC: Primitive::clc(context); return; + case Operation::CLD: Primitive::cld(context); return; + case Operation::CLI: Primitive::cli(context); return; + case Operation::STC: Primitive::stc(context); return; + case Operation::STD: Primitive::std(context); return; + case Operation::STI: Primitive::sti(context); return; + case Operation::CMC: Primitive::cmc(context); return; + + case Operation::XCHG: Primitive::xchg(destination_rmw(), source_rmw()); break; + + case Operation::SALC: Primitive::salc(context.registers.al(), context); return; + case Operation::SETMO: + if constexpr (ContextT::model == Model::i8086) { + Primitive::setmo(destination_w(), context); + break; + } else { + static_assert(int(Operation::SETMO) == int(Operation::ENTER)); + Primitive::enter(instruction, context); + } + return; + case Operation::SETMOC: + if constexpr (ContextT::model == Model::i8086) { + // Test CL out here to avoid taking a reference to memory if + // no write is going to occur. + if(context.registers.cl()) { + Primitive::setmo(destination_w(), context); + } + break; + } else { + static_assert(int(Operation::SETMOC) == int(Operation::BOUND)); + Primitive::bound(instruction, destination_r(), source_r(), context); + } + return; + + case Operation::OUT: Primitive::out(port(instruction.destination().source()), pair_low(), context); return; + case Operation::IN: Primitive::in(port(instruction.source().source()), pair_low(), context); return; + + case Operation::XLAT: Primitive::xlat(instruction, context); return; + + case Operation::POP: + destination_w() = Primitive::pop(context); + if constexpr (std::is_same_v) { + context.segments.did_update(instruction.destination().source()); + } + break; + case Operation::PUSH: + Primitive::push(source_rmw(), context); // PUSH SP modifies SP before pushing it; + // hence PUSH is sometimes read-modify-write. + break; + + case Operation::POPF: + if constexpr (std::is_same_v || std::is_same_v) { + Primitive::popf(context); + } + return; + case Operation::PUSHF: + if constexpr (std::is_same_v || std::is_same_v) { + Primitive::pushf(context); + } + return; + case Operation::POPA: + if constexpr (std::is_same_v || std::is_same_v) { + Primitive::popa(context); + } + return; + case Operation::PUSHA: + if constexpr (std::is_same_v || std::is_same_v) { + Primitive::pusha(context); + } + return; + + case Operation::CMPS: + Primitive::cmps(instruction, eCX(), eSI(), eDI(), context); + return; + case Operation::CMPS_REPE: + Primitive::cmps(instruction, eCX(), eSI(), eDI(), context); + return; + case Operation::CMPS_REPNE: + Primitive::cmps(instruction, eCX(), eSI(), eDI(), context); + return; + + case Operation::SCAS: + Primitive::scas(eCX(), eDI(), pair_low(), context); + return; + case Operation::SCAS_REPE: + Primitive::scas(eCX(), eDI(), pair_low(), context); + return; + case Operation::SCAS_REPNE: + Primitive::scas(eCX(), eDI(), pair_low(), context); + return; + + case Operation::LODS: + Primitive::lods(instruction, eCX(), eSI(), pair_low(), context); + return; + case Operation::LODS_REP: + Primitive::lods(instruction, eCX(), eSI(), pair_low(), context); + return; + + case Operation::MOVS: + Primitive::movs(instruction, eCX(), eSI(), eDI(), context); + break; + case Operation::MOVS_REP: + Primitive::movs(instruction, eCX(), eSI(), eDI(), context); + break; + + case Operation::STOS: + Primitive::stos(eCX(), eDI(), pair_low(), context); + break; + case Operation::STOS_REP: + Primitive::stos(eCX(), eDI(), pair_low(), context); + break; + + case Operation::OUTS: + Primitive::outs(instruction, eCX(), context.registers.dx(), eSI(), context); + return; + case Operation::OUTS_REP: + Primitive::outs(instruction, eCX(), context.registers.dx(), eSI(), context); + return; + + case Operation::INS: + Primitive::ins(eCX(), context.registers.dx(), eDI(), context); + break; + case Operation::INS_REP: + Primitive::ins(eCX(), context.registers.dx(), eDI(), context); + break; + } + + // Write to memory if required to complete this operation. + // + // This is not currently handled via RAII because of the amount of context that would need to place onto the stack; + // instead code has been set up to make sure there is only at most one writeable target on loan for potential + // write back. I might flip-flop on this, especially if I can verify whether extra stack context is easily + // optimised out. + context.memory.template write_back(); +} + +// +// Public function; just a trampoline into a version of perform templated on data and address size. +// +// Which, yes, means there's an outer switch leading to an inner switch, which could be reduced to one big switch. +// It'd be a substantial effort to find the most neat expression of that, I think, so it is not currently done. +// +template < + typename InstructionT, + typename ContextT +> void perform( + const InstructionT &instruction, + ContextT &context +) { + auto size = [](DataSize operation_size, AddressSize address_size) constexpr -> int { + return int(operation_size) + (int(address_size) << 2); + }; + + // Dispatch to a function specialised on data and address size. + switch(size(instruction.operation_size(), instruction.address_size())) { + // 16-bit combinations. + case size(DataSize::Byte, AddressSize::b16): + perform(instruction, context); + return; + case size(DataSize::Word, AddressSize::b16): + perform(instruction, context); + return; + + // 32-bit combinations. + // + // The if constexprs below ensure that `perform` isn't compiled for incompatible data or address size and + // model combinations. So if a caller nominates a 16-bit model it can supply registers and memory objects + // that don't implement 32-bit registers or accesses. + case size(DataSize::Byte, AddressSize::b32): + if constexpr (is_32bit(ContextT::model)) { + perform(instruction, context); + return; + } + break; + case size(DataSize::Word, AddressSize::b32): + if constexpr (is_32bit(ContextT::model)) { + perform(instruction, context); + return; + } + break; + case size(DataSize::DWord, AddressSize::b16): + if constexpr (is_32bit(ContextT::model)) { + perform(instruction, context); + return; + } + break; + case size(DataSize::DWord, AddressSize::b32): + if constexpr (is_32bit(ContextT::model)) { + perform(instruction, context); + return; + } + break; + + default: break; + } + + // This is reachable only if the data and address size combination in use isn't available + // on the processor model nominated. + assert(false); +} + +template < + typename ContextT +> void interrupt( + int index, + ContextT &context +) { + const uint32_t address = static_cast(index) << 2; + context.memory.preauthorise_read(address, sizeof(uint16_t) * 2); + context.memory.preauthorise_stack_write(sizeof(uint16_t) * 3); + + const uint16_t ip = context.memory.template access(address); + const uint16_t cs = context.memory.template access(address + 2); + + auto flags = context.flags.get(); + Primitive::push(flags, context); + context.flags.template set_from(0); + + // Push CS and IP. + Primitive::push(context.registers.cs(), context); + Primitive::push(context.registers.ip(), context); + + // Set new destination. + context.flow_controller.jump(cs, ip); +} + +} diff --git a/InstructionSets/x86/Implementation/Repetition.hpp b/InstructionSets/x86/Implementation/Repetition.hpp new file mode 100644 index 000000000..01e6c1d48 --- /dev/null +++ b/InstructionSets/x86/Implementation/Repetition.hpp @@ -0,0 +1,177 @@ +// +// Repetition.hpp +// Clock Signal +// +// Created by Thomas Harte on 08/11/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "../AccessType.hpp" + +namespace InstructionSet::x86::Primitive { + +template +bool repetition_over( + const AddressT &eCX +) { + return repetition != Repetition::None && !eCX; +} + +template +void repeat( + AddressT &eCX, + ContextT &context +) { + if( + repetition == Repetition::None || // No repetition => stop. + !(--eCX) // [e]cx is zero after being decremented => stop. + ) { + return; + } + if constexpr (repetition != Repetition::Rep) { + // If this is RepE or RepNE, also test the zero flag. + if((repetition == Repetition::RepNE) == context.flags.template flag()) { + return; + } + } + context.flow_controller.repeat_last(); +} + +template +void cmps( + const InstructionT &instruction, + AddressT &eCX, + AddressT &eSI, + AddressT &eDI, + ContextT &context +) { + if(repetition_over(eCX)) { + return; + } + + IntT lhs = context.memory.template access(instruction.data_segment(), eSI); + const IntT rhs = context.memory.template access(Source::ES, eDI); + eSI += context.flags.template direction() * sizeof(IntT); + eDI += context.flags.template direction() * sizeof(IntT); + + Primitive::sub(lhs, rhs, context); + + repeat(eCX, context); +} + +template +void scas( + AddressT &eCX, + AddressT &eDI, + IntT &eAX, + ContextT &context +) { + if(repetition_over(eCX)) { + return; + } + + const IntT rhs = context.memory.template access(Source::ES, eDI); + eDI += context.flags.template direction() * sizeof(IntT); + + Primitive::sub(eAX, rhs, context); + + repeat(eCX, context); +} + +template +void lods( + const InstructionT &instruction, + AddressT &eCX, + AddressT &eSI, + IntT &eAX, + ContextT &context +) { + if(repetition_over(eCX)) { + return; + } + + eAX = context.memory.template access(instruction.data_segment(), eSI); + eSI += context.flags.template direction() * sizeof(IntT); + + repeat(eCX, context); +} + +template +void movs( + const InstructionT &instruction, + AddressT &eCX, + AddressT &eSI, + AddressT &eDI, + ContextT &context +) { + if(repetition_over(eCX)) { + return; + } + + context.memory.template access(Source::ES, eDI) = + context.memory.template access(instruction.data_segment(), eSI); + + eSI += context.flags.template direction() * sizeof(IntT); + eDI += context.flags.template direction() * sizeof(IntT); + + repeat(eCX, context); +} + +template +void stos( + AddressT &eCX, + AddressT &eDI, + IntT &eAX, + ContextT &context +) { + if(repetition_over(eCX)) { + return; + } + + context.memory.template access(Source::ES, eDI) = eAX; + eDI += context.flags.template direction() * sizeof(IntT); + + repeat(eCX, context); +} + +template +void outs( + const InstructionT &instruction, + AddressT &eCX, + uint16_t port, + AddressT &eSI, + ContextT &context +) { + if(repetition_over(eCX)) { + return; + } + + context.io.template out( + port, + context.memory.template access(instruction.data_segment(), eSI) + ); + eSI += context.flags.template direction() * sizeof(IntT); + + repeat(eCX, context); +} + +template +void ins( + AddressT &eCX, + uint16_t port, + AddressT &eDI, + ContextT &context +) { + if(repetition_over(eCX)) { + return; + } + + context.memory.template access(Source::ES, eDI) = context.io.template in(port); + eDI += context.flags.template direction() * sizeof(IntT); + + repeat(eCX, context); +} + +} diff --git a/InstructionSets/x86/Implementation/Resolver.hpp b/InstructionSets/x86/Implementation/Resolver.hpp new file mode 100644 index 000000000..4d71a881c --- /dev/null +++ b/InstructionSets/x86/Implementation/Resolver.hpp @@ -0,0 +1,206 @@ +// +// Resolver.hpp +// Clock Signal +// +// Created by Thomas Harte on 05/11/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "../AccessType.hpp" + +namespace InstructionSet::x86 { + +/// Obtain a pointer to the value desribed by @c source, which is one of those named by @c pointer, using @c instruction and @c context +/// for offsets, registers and memory contents. +/// +/// If @c source is Source::None then @c none is returned. +/// +/// If @c source is Source::Immediate then the appropriate portion of @c instrucion's operand +/// is copied to @c *immediate and @c immediate is returned. +template +typename Accessor::type resolve( + InstructionT &instruction, + Source source, + DataPointer pointer, + ContextT &context, + IntT *none = nullptr, + IntT *immediate = nullptr +); + +/// Calculates the absolute address for @c pointer given the registers and memory provided in @c context and taking any +/// referenced offset from @c instruction. +template +uint32_t address( + InstructionT &instruction, + DataPointer pointer, + ContextT &context +) { + if constexpr (source == Source::DirectAddress) { + return instruction.offset(); + } + + uint32_t address; + uint16_t zero = 0; + address = resolve(instruction, pointer.index(), pointer, context, &zero); + if constexpr (is_32bit(ContextT::model)) { + address <<= pointer.scale(); + } + address += instruction.offset(); + + if constexpr (source == Source::IndirectNoBase) { + return address; + } + return address + resolve(instruction, pointer.base(), pointer, context); +} + +/// @returns a pointer to the contents of the register identified by the combination of @c IntT and @c Source if any; +/// @c nullptr otherwise. @c access is currently unused but is intended to provide the hook upon which updates to +/// segment registers can be tracked for protected modes. +template +IntT *register_(ContextT &context) { + static constexpr bool supports_dword = is_32bit(ContextT::model); + + switch(source) { + case Source::eAX: + // Slightly contorted if chain here and below: + // + // (i) does the `constexpr` version of a `switch`; and + // (i) ensures .eax() etc aren't called on @c registers for 16-bit processors, so they need not implement 32-bit storage. + if constexpr (supports_dword && std::is_same_v) { return &context.registers.eax(); } + else if constexpr (std::is_same_v) { return &context.registers.ax(); } + else if constexpr (std::is_same_v) { return &context.registers.al(); } + else { return nullptr; } + case Source::eCX: + if constexpr (supports_dword && std::is_same_v) { return &context.registers.ecx(); } + else if constexpr (std::is_same_v) { return &context.registers.cx(); } + else if constexpr (std::is_same_v) { return &context.registers.cl(); } + else { return nullptr; } + case Source::eDX: + if constexpr (supports_dword && std::is_same_v) { return &context.registers.edx(); } + else if constexpr (std::is_same_v) { return &context.registers.dx(); } + else if constexpr (std::is_same_v) { return &context.registers.dl(); } + else if constexpr (std::is_same_v) { return nullptr; } + case Source::eBX: + if constexpr (supports_dword && std::is_same_v) { return &context.registers.ebx(); } + else if constexpr (std::is_same_v) { return &context.registers.bx(); } + else if constexpr (std::is_same_v) { return &context.registers.bl(); } + else if constexpr (std::is_same_v) { return nullptr; } + case Source::eSPorAH: + if constexpr (supports_dword && std::is_same_v) { return &context.registers.esp(); } + else if constexpr (std::is_same_v) { return &context.registers.sp(); } + else if constexpr (std::is_same_v) { return &context.registers.ah(); } + else { return nullptr; } + case Source::eBPorCH: + if constexpr (supports_dword && std::is_same_v) { return &context.registers.ebp(); } + else if constexpr (std::is_same_v) { return &context.registers.bp(); } + else if constexpr (std::is_same_v) { return &context.registers.ch(); } + else { return nullptr; } + case Source::eSIorDH: + if constexpr (supports_dword && std::is_same_v) { return &context.registers.esi(); } + else if constexpr (std::is_same_v) { return &context.registers.si(); } + else if constexpr (std::is_same_v) { return &context.registers.dh(); } + else { return nullptr; } + case Source::eDIorBH: + if constexpr (supports_dword && std::is_same_v) { return &context.registers.edi(); } + else if constexpr (std::is_same_v) { return &context.registers.di(); } + else if constexpr (std::is_same_v) { return &context.registers.bh(); } + else { return nullptr; } + + // Segment registers are always 16-bit. + case Source::ES: if constexpr (std::is_same_v) return &context.registers.es(); else return nullptr; + case Source::CS: if constexpr (std::is_same_v) return &context.registers.cs(); else return nullptr; + case Source::SS: if constexpr (std::is_same_v) return &context.registers.ss(); else return nullptr; + case Source::DS: if constexpr (std::is_same_v) return &context.registers.ds(); else return nullptr; + + // 16-bit models don't have FS and GS. + case Source::FS: if constexpr (is_32bit(ContextT::model) && std::is_same_v) return &context.registers.fs(); else return nullptr; + case Source::GS: if constexpr (is_32bit(ContextT::model) && std::is_same_v) return &context.registers.gs(); else return nullptr; + + default: return nullptr; + } +} + +///Obtains the address described by @c pointer from @c instruction given the registers and memory as described by @c context. +template +uint32_t address( + InstructionT &instruction, + DataPointer pointer, + ContextT &context +) { + // TODO: at least on the 8086 this isn't how register 'addresses' are resolved; instead whatever was the last computed address + // remains in the address register and is returned. Find out what other x86s do and make a decision. + switch(pointer.source()) { + default: return 0; + case Source::eAX: return *register_(context); + case Source::eCX: return *register_(context); + case Source::eDX: return *register_(context); + case Source::eBX: return *register_(context); + case Source::eSPorAH: return *register_(context); + case Source::eBPorCH: return *register_(context); + case Source::eSIorDH: return *register_(context); + case Source::eDIorBH: return *register_(context); + case Source::Indirect: return address(instruction, pointer, context); + case Source::IndirectNoBase: return address(instruction, pointer, context); + case Source::DirectAddress: return address(instruction, pointer, context); + } +} + +// See forward declaration, above, for details. +template +typename Accessor::type resolve( + InstructionT &instruction, + Source source, + DataPointer pointer, + ContextT &context, + IntT *none, + IntT *immediate +) { + // Rules: + // + // * if this is a memory access, set target_address and break; + // * otherwise return the appropriate value. + uint32_t target_address = 0; + switch(source) { + // Defer all register accesses to the register-specific lookup. + case Source::eAX: return *register_(context); + case Source::eCX: return *register_(context); + case Source::eDX: return *register_(context); + case Source::eBX: return *register_(context); + case Source::eSPorAH: return *register_(context); + case Source::eBPorCH: return *register_(context); + case Source::eSIorDH: return *register_(context); + case Source::eDIorBH: return *register_(context); + case Source::ES: return *register_(context); + case Source::CS: return *register_(context); + case Source::SS: return *register_(context); + case Source::DS: return *register_(context); + case Source::FS: return *register_(context); + case Source::GS: return *register_(context); + + case Source::None: return *none; + + case Source::Immediate: + *immediate = IntT(instruction.operand()); + return *immediate; + + case Source::Indirect: + target_address = address(instruction, pointer, context); + break; + case Source::IndirectNoBase: + target_address = address(instruction, pointer, context); + break; + case Source::DirectAddress: + target_address = address(instruction, pointer, context); + break; + } + + // If execution has reached here then a memory fetch is required. + // Do it and exit. + // + // TODO: support 32-bit addresses. + return context.memory.template access(instruction.data_segment(), uint16_t(target_address)); +} + +} diff --git a/InstructionSets/x86/Implementation/ShiftRoll.hpp b/InstructionSets/x86/Implementation/ShiftRoll.hpp new file mode 100644 index 000000000..49ce02209 --- /dev/null +++ b/InstructionSets/x86/Implementation/ShiftRoll.hpp @@ -0,0 +1,362 @@ +// +// ShiftRoll.hpp +// Clock Signal +// +// Created by Thomas Harte on 08/11/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "../AccessType.hpp" + +namespace InstructionSet::x86::Primitive { + +template +void rcl( + modify_t destination, + uint8_t count, + ContextT &context +) { + /* + (* RCL and RCR instructions *) + SIZE ← OperandSize + CASE (determine count) OF + SIZE = 8: tempCOUNT ← (COUNT AND 1FH) MOD 9; + SIZE = 16: tempCOUNT ← (COUNT AND 1FH) MOD 17; + SIZE = 32: tempCOUNT ← COUNT AND 1FH; + ESAC; + */ + /* + (* RCL instruction operation *) + WHILE (tempCOUNT ≠ 0) + DO + tempCF ← MSB(DEST); + DEST ← (DEST * 2) + CF; + CF ← tempCF; + tempCOUNT ← tempCOUNT – 1; + OD; + ELIHW; + IF COUNT = 1 + THEN OF ← MSB(DEST) XOR CF; + ELSE OF is undefined; + FI; + */ + /* + The CF flag contains the value of the bit shifted into it. + The OF flag is affected only for single- bit rotates (see “Description” above); + it is undefined for multi-bit rotates. The SF, ZF, AF, and PF flags are not affected. + */ + const auto temp_count = count % (Numeric::bit_size() + 1); + auto carry = context.flags.template carry_bit(); + switch(temp_count) { + case 0: break; + case Numeric::bit_size(): { + const IntT temp_carry = destination & 1; + destination = IntT((destination >> 1) | (carry << (Numeric::bit_size() - 1))); + carry = temp_carry; + } break; + default: { + const IntT temp_carry = destination & (Numeric::top_bit() >> (temp_count - 1)); + destination = IntT( + (destination << temp_count) | + (destination >> (Numeric::bit_size() + 1 - temp_count)) | + (carry << (temp_count - 1)) + ); + carry = temp_carry ? 1 : 0; + } break; + } + + context.flags.template set_from(carry); + context.flags.template set_from( + ((destination >> (Numeric::bit_size() - 1)) & 1) ^ carry + ); +} + +template +void rcr( + modify_t destination, + uint8_t count, + ContextT &context +) { + /* + (* RCR instruction operation *) + IF COUNT = 1 + THEN OF ← MSB(DEST) XOR CF; + ELSE OF is undefined; + FI; + WHILE (tempCOUNT ≠ 0) + DO + tempCF ← LSB(SRC); + DEST ← (DEST / 2) + (CF * 2SIZE); + CF ← tempCF; + tempCOUNT ← tempCOUNT – 1; + OD; + */ + auto carry = context.flags.template carry_bit(); + context.flags.template set_from( + ((destination >> (Numeric::bit_size() - 1)) & 1) ^ carry + ); + + const auto temp_count = count % (Numeric::bit_size() + 1); + switch(temp_count) { + case 0: break; + case Numeric::bit_size(): { + const IntT temp_carry = destination & Numeric::top_bit(); + destination = IntT((destination << 1) | carry); + carry = temp_carry; + } break; + default: { + const IntT temp_carry = destination & (1 << (temp_count - 1)); + destination = IntT( + (destination >> temp_count) | + (destination << (Numeric::bit_size() + 1 - temp_count)) | + (carry << (Numeric::bit_size() - temp_count)) + ); + carry = temp_carry; + } break; + } + + context.flags.template set_from(carry); +} + +template +void rol( + modify_t destination, + uint8_t count, + ContextT &context +) { + /* + (* ROL and ROR instructions *) + SIZE ← OperandSize + CASE (determine count) OF + SIZE = 8: tempCOUNT ← COUNT MOD 8; + SIZE = 16: tempCOUNT ← COUNT MOD 16; + SIZE = 32: tempCOUNT ← COUNT MOD 32; + ESAC; + */ + /* + (* ROL instruction operation *) + WHILE (tempCOUNT ≠ 0) + DO + tempCF ← MSB(DEST); + DEST ← (DEST * 2) + tempCF; + tempCOUNT ← tempCOUNT – 1; + OD; + ELIHW; + IF COUNT = 1 + THEN OF ← MSB(DEST) XOR CF; + ELSE OF is undefined; + FI; + */ + /* + The CF flag contains the value of the bit shifted into it. + The OF flag is affected only for single- bit rotates (see “Description” above); + it is undefined for multi-bit rotates. The SF, ZF, AF, and PF flags are not affected. + */ + const auto temp_count = count & (Numeric::bit_size() - 1); + if(!count) { + // TODO: is this 8086-specific? i.e. do the other x86s also exit without affecting flags when temp_count = 0? + return; + } + if(temp_count) { + destination = IntT( + (destination << temp_count) | + (destination >> (Numeric::bit_size() - temp_count)) + ); + } + + context.flags.template set_from(destination & 1); + context.flags.template set_from( + ((destination >> (Numeric::bit_size() - 1)) ^ destination) & 1 + ); +} + +template +void ror( + modify_t destination, + uint8_t count, + ContextT &context +) { + /* + (* ROL and ROR instructions *) + SIZE ← OperandSize + CASE (determine count) OF + SIZE = 8: tempCOUNT ← COUNT MOD 8; + SIZE = 16: tempCOUNT ← COUNT MOD 16; + SIZE = 32: tempCOUNT ← COUNT MOD 32; + ESAC; + */ + /* + (* ROR instruction operation *) + WHILE (tempCOUNT ≠ 0) + DO + tempCF ← LSB(DEST); + DEST ← (DEST / 2) + (tempCF * 2^SIZE); + tempCOUNT ← tempCOUNT – 1; + OD; + ELIHW; + IF COUNT = 1 + THEN OF ← MSB(DEST) XOR MSB - 1 (DEST); + ELSE OF is undefined; + FI; + */ + /* + The CF flag contains the value of the bit shifted into it. + The OF flag is affected only for single- bit rotates (see “Description” above); + it is undefined for multi-bit rotates. The SF, ZF, AF, and PF flags are not affected. + */ + const auto temp_count = count & (Numeric::bit_size() - 1); + if(!count) { + // TODO: is this 8086-specific? i.e. do the other x86s also exit without affecting flags when temp_count = 0? + return; + } + if(temp_count) { + destination = IntT( + (destination >> temp_count) | + (destination << (Numeric::bit_size() - temp_count)) + ); + } + + context.flags.template set_from(destination & Numeric::top_bit()); + context.flags.template set_from( + (destination ^ (destination << 1)) & Numeric::top_bit() + ); +} + +/* + tempCOUNT ← (COUNT AND 1FH); + tempDEST ← DEST; + WHILE (tempCOUNT ≠ 0) + DO + IF instruction is SAL or SHL + THEN + CF ← MSB(DEST); + ELSE (* instruction is SAR or SHR *) + CF ← LSB(DEST); + FI; + IF instruction is SAL or SHL + THEN + DEST ← DEST ∗ 2; + ELSE + IF instruction is SAR + THEN + DEST ← DEST / 2 (*Signed divide, rounding toward negative infinity*); + ELSE (* instruction is SHR *) + DEST ← DEST / 2 ; (* Unsigned divide *); + FI; + FI; + tempCOUNT ← tempCOUNT – 1; + OD; + (* Determine overflow for the various instructions *) + IF COUNT = 1 + THEN + IF instruction is SAL or SHL + THEN + OF ← MSB(DEST) XOR CF; + ELSE + IF instruction is SAR + THEN + OF ← 0; + ELSE (* instruction is SHR *) + OF ← MSB(tempDEST); + FI; + FI; + ELSE + IF COUNT = 0 + THEN + All flags remain unchanged; + ELSE (* COUNT neither 1 or 0 *) + OF ← undefined; + FI; + FI; +*/ +/* + The CF flag contains the value of the last bit shifted out of the destination operand; + it is undefined for SHL and SHR instructions where the count is greater than or equal to + the size (in bits) of the destination operand. The OF flag is affected only for 1-bit shifts + (see “Description” above); otherwise, it is undefined. + + The SF, ZF, and PF flags are set according to the result. If the count is 0, the flags are not affected. + For a non-zero count, the AF flag is undefined. +*/ +template +void sal( + modify_t destination, + uint8_t count, + ContextT &context +) { + switch(count) { + case 0: return; + case Numeric::bit_size(): + context.flags.template set_from(destination & 1); + destination = 0; + break; + default: + if(count > Numeric::bit_size()) { + context.flags.template set_from(0); + destination = 0; + } else { + const auto mask = (Numeric::top_bit() >> (count - 1)); + context.flags.template set_from( + destination & mask + ); + context.flags.template set_from(IntT( + (destination ^ (destination << 1)) & mask + )); + destination <<= count; + } + break; + } + context.flags.template set_from(destination); +} + +template +void sar( + modify_t destination, + uint8_t count, + ContextT &context +) { + if(!count) { + return; + } + + const IntT sign = Numeric::top_bit() & destination; + if(count >= Numeric::bit_size()) { + destination = sign ? IntT(~0) : IntT(0); + context.flags.template set_from(sign); + } else { + const auto mask = IntT(1 << (count - 1)); + context.flags.template set_from(destination & mask); + destination = (destination >> count) | (sign ? ~(IntT(~0) >> count) : 0); + } + context.flags.template set_from(0); + context.flags.template set_from(destination); +} + +template +void shr( + modify_t destination, + uint8_t count, + ContextT &context +) { + if(!count) { + return; + } + + context.flags.template set_from(Numeric::top_bit() & destination); + if(count == Numeric::bit_size()) { + context.flags.template set_from(Numeric::top_bit() & destination); + destination = 0; + } else if(count > Numeric::bit_size()) { + context.flags.template set_from(0); + destination = 0; + } else { + const auto mask = IntT(1 << (count - 1)); + context.flags.template set_from(destination & mask); + destination >>= count; + } + context.flags.template set_from(destination); +} + +} diff --git a/InstructionSets/x86/Implementation/Stack.hpp b/InstructionSets/x86/Implementation/Stack.hpp new file mode 100644 index 000000000..6568d2040 --- /dev/null +++ b/InstructionSets/x86/Implementation/Stack.hpp @@ -0,0 +1,198 @@ +// +// Stack.hpp +// Clock Signal +// +// Created by Thomas Harte on 08/11/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "../AccessType.hpp" + +#include + +namespace InstructionSet::x86::Primitive { + +// The below takes a reference in order properly to handle PUSH SP, +// which should place the value of SP after the push onto the stack. +template +void push( + IntT &value, + ContextT &context +) { + context.registers.sp() -= sizeof(IntT); + if constexpr (preauthorised) { + context.memory.template preauthorised_write(Source::SS, context.registers.sp(), value); + } else { + context.memory.template access( + Source::SS, + context.registers.sp()) = value; + } + context.memory.template write_back(); +} + +template +IntT pop( + ContextT &context +) { + const auto value = context.memory.template access( + Source::SS, + context.registers.sp()); + context.registers.sp() += sizeof(IntT); + return value; +} + +template +void sahf( + uint8_t &ah, + ContextT &context +) { + /* + EFLAGS(SF:ZF:0:AF:0:PF:1:CF) ← AH; + */ + context.flags.template set_from(ah); + context.flags.template set_from(!(ah & 0x40)); + context.flags.template set_from(ah & 0x10); + context.flags.template set_from(!(ah & 0x04)); + context.flags.template set_from(ah & 0x01); +} + +template +void lahf( + uint8_t &ah, + ContextT &context +) { + /* + AH ← EFLAGS(SF:ZF:0:AF:0:PF:1:CF); + */ + ah = + (context.flags.template flag() ? 0x80 : 0x00) | + (context.flags.template flag() ? 0x40 : 0x00) | + (context.flags.template flag() ? 0x10 : 0x00) | + (context.flags.template flag() ? 0x00 : 0x04) | + 0x02 | + (context.flags.template flag() ? 0x01 : 0x00); +} + +template +void popf( + ContextT &context +) { + context.flags.set(pop(context)); +} + +template +void pushf( + ContextT &context +) { + uint16_t value = context.flags.get(); + push(value, context); +} + +template +void popa( + ContextT &context +) { + context.memory.preauthorise_stack_read(sizeof(IntT) * 8); + if constexpr (std::is_same_v) { + context.registers.edi() = pop(context); + context.registers.esi() = pop(context); + context.registers.ebp() = pop(context); + context.registers.esp() += 4; + context.registers.ebx() = pop(context); + context.registers.edx() = pop(context); + context.registers.ecx() = pop(context); + context.registers.eax() = pop(context); + } else { + context.registers.di() = pop(context); + context.registers.si() = pop(context); + context.registers.bp() = pop(context); + context.registers.sp() += 2; + context.registers.bx() = pop(context); + context.registers.dx() = pop(context); + context.registers.cx() = pop(context); + context.registers.ax() = pop(context); + } +} + +template +void pusha( + ContextT &context +) { + context.memory.preauthorise_stack_read(sizeof(IntT) * 8); + IntT initial_sp = context.registers.sp(); + if constexpr (std::is_same_v) { + push(context.registers.eax(), context); + push(context.registers.ecx(), context); + push(context.registers.edx(), context); + push(context.registers.ebx(), context); + push(initial_sp, context); + push(context.registers.ebp(), context); + push(context.registers.esi(), context); + push(context.registers.esi(), context); + } else { + push(context.registers.ax(), context); + push(context.registers.cx(), context); + push(context.registers.dx(), context); + push(context.registers.bx(), context); + push(initial_sp, context); + push(context.registers.bp(), context); + push(context.registers.si(), context); + push(context.registers.si(), context); + } +} + +template +void enter( + const InstructionT &instruction, + ContextT &context +) { + // TODO: all non-16bit address sizes. + const auto alloc_size = instruction.dynamic_storage_size(); + const auto nesting_level = instruction.nesting_level() & 0x1f; + + // Preauthorse contents that'll be fetched via BP. + const auto copied_pointers = nesting_level - 2; + if(copied_pointers > 0) { + context.memory.preauthorise_read( + Source::SS, + context.registers.bp() - copied_pointers * sizeof(uint16_t), + copied_pointers * sizeof(uint16_t) + ); + } + + // Preauthorse stack activity. + context.memory.preauthorise_stack_write((1 + copied_pointers) * sizeof(uint16_t)); + + // Push BP and grab the end of frame. + push(context.registers.bp(), context); + const auto frame = context.registers.sp(); + + // Copy data as per the nesting level. + for(int c = 1; c < nesting_level; c++) { + context.registers.bp() -= 2; + + const auto value = context.memory.template preauthorised_read(Source::SS, context.registers.bp()); + push(value); + } + + // Set final BP. + context.registers.bp() = frame; +} + +template +void leave( + ContextT &context +) { + // TODO: should use StackAddressSize to determine assignment of bp to sp. + if constexpr (std::is_same_v) { + context.registers.esp() = context.registers.ebp(); + context.registers.ebp() = pop(context); + } else { + context.registers.sp() = context.registers.bp(); + context.registers.bp() = pop(context); + } +} + +} diff --git a/InstructionSets/x86/Instruction.cpp b/InstructionSets/x86/Instruction.cpp new file mode 100644 index 000000000..edcf2b738 --- /dev/null +++ b/InstructionSets/x86/Instruction.cpp @@ -0,0 +1,618 @@ +// +// Instruction.cpp +// Clock Signal +// +// Created by Thomas Harte on 17/09/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#include "Instruction.hpp" + +#include "../../Numeric/Carry.hpp" + +#include +#include +#include + +using namespace InstructionSet::x86; + +bool InstructionSet::x86::has_displacement(Operation operation) { + switch(operation) { + default: return false; + + case Operation::JO: case Operation::JNO: + case Operation::JB: case Operation::JNB: + case Operation::JZ: case Operation::JNZ: + case Operation::JBE: case Operation::JNBE: + case Operation::JS: case Operation::JNS: + case Operation::JP: case Operation::JNP: + case Operation::JL: case Operation::JNL: + case Operation::JLE: case Operation::JNLE: + case Operation::LOOPNE: case Operation::LOOPE: + case Operation::LOOP: case Operation::JCXZ: + case Operation::CALLrel: case Operation::JMPrel: + return true; + } +} + +int InstructionSet::x86::max_displayed_operands(Operation operation) { + switch(operation) { + default: return 2; + + case Operation::INC: case Operation::DEC: + case Operation::POP: case Operation::PUSH: + case Operation::MUL: case Operation::IMUL_1: + case Operation::IDIV: case Operation::DIV: + case Operation::ESC: + case Operation::AAM: case Operation::AAD: + case Operation::INT: + case Operation::JMPabs: case Operation::JMPfar: + case Operation::CALLabs: case Operation::CALLfar: + case Operation::NEG: case Operation::NOT: + case Operation::RETnear: + case Operation::RETfar: + return 1; + + // Pedantically, these have an displacement rather than an operand. + case Operation::JO: case Operation::JNO: + case Operation::JB: case Operation::JNB: + case Operation::JZ: case Operation::JNZ: + case Operation::JBE: case Operation::JNBE: + case Operation::JS: case Operation::JNS: + case Operation::JP: case Operation::JNP: + case Operation::JL: case Operation::JNL: + case Operation::JLE: case Operation::JNLE: + case Operation::LOOPNE: case Operation::LOOPE: + case Operation::LOOP: case Operation::JCXZ: + case Operation::CALLrel: case Operation::JMPrel: + // Genuine zero-operand instructions: + case Operation::CMPS: case Operation::LODS: + case Operation::MOVS: case Operation::SCAS: + case Operation::STOS: + case Operation::CLC: case Operation::CLD: + case Operation::CLI: + case Operation::STC: case Operation::STD: + case Operation::STI: + case Operation::CMC: + case Operation::LAHF: case Operation::SAHF: + case Operation::AAA: case Operation::AAS: + case Operation::DAA: case Operation::DAS: + case Operation::CBW: case Operation::CWD: + case Operation::INTO: + case Operation::PUSHF: case Operation::POPF: + case Operation::IRET: + case Operation::NOP: + case Operation::XLAT: + case Operation::SALC: + case Operation::Invalid: + return 0; + } +} + +std::string InstructionSet::x86::to_string(Operation operation, DataSize size, Model model) { + switch(operation) { + case Operation::AAA: return "aaa"; + case Operation::AAD: return "aad"; + case Operation::AAM: return "aam"; + case Operation::AAS: return "aas"; + case Operation::DAA: return "daa"; + case Operation::DAS: return "das"; + + case Operation::CBW: return "cbw"; + case Operation::CWD: return "cwd"; + case Operation::ESC: return "esc"; + + case Operation::HLT: return "hlt"; + case Operation::WAIT: return "wait"; + + case Operation::ADC: return "adc"; + case Operation::ADD: return "add"; + case Operation::SBB: return "sbb"; + case Operation::SUB: return "sub"; + case Operation::MUL: return "mul"; + case Operation::IMUL_1: return "imul"; + case Operation::DIV: return "div"; + case Operation::IDIV: return "idiv"; + case Operation::IDIV_REP: return "idiv"; + + case Operation::INC: return "inc"; + case Operation::DEC: return "dec"; + + case Operation::IN: return "in"; + case Operation::OUT: return "out"; + + case Operation::JO: return "jo"; + case Operation::JNO: return "jno"; + case Operation::JB: return "jb"; + case Operation::JNB: return "jnb"; + case Operation::JZ: return "jz"; + case Operation::JNZ: return "jnz"; + case Operation::JBE: return "jbe"; + case Operation::JNBE: return "jnbe"; + case Operation::JS: return "js"; + case Operation::JNS: return "jns"; + case Operation::JP: return "jp"; + case Operation::JNP: return "jnp"; + case Operation::JL: return "jl"; + case Operation::JNL: return "jnl"; + case Operation::JLE: return "jle"; + case Operation::JNLE: return "jnle"; + + case Operation::CALLabs: return "call"; + case Operation::CALLrel: return "call"; + case Operation::CALLfar: return "callf"; + case Operation::IRET: return "iret"; + case Operation::RETfar: return "retf"; + case Operation::RETnear: return "retn"; + case Operation::JMPabs: return "jmp"; + case Operation::JMPrel: return "jmp"; + case Operation::JMPfar: return "jmpf"; + case Operation::JCXZ: return "jcxz"; + case Operation::INT: return "int"; + case Operation::INTO: return "into"; + + case Operation::LAHF: return "lahf"; + case Operation::SAHF: return "sahf"; + case Operation::LDS: return "lds"; + case Operation::LES: return "les"; + case Operation::LEA: return "lea"; + + case Operation::CMPS: { + constexpr char sizes[][6] = { "cmpsb", "cmpsw", "cmpsd", "?" }; + return sizes[static_cast(size)]; + } + case Operation::CMPS_REPE: { + constexpr char sizes[][11] = { "repe cmpsb", "repe cmpsw", "repe cmpsd", "?" }; + return sizes[static_cast(size)]; + } + case Operation::CMPS_REPNE: { + constexpr char sizes[][12] = { "repne cmpsb", "repne cmpsw", "repne cmpsd", "?" }; + return sizes[static_cast(size)]; + } + + case Operation::SCAS: { + constexpr char sizes[][6] = { "scasb", "scasw", "scasd", "?" }; + return sizes[static_cast(size)]; + } + case Operation::SCAS_REPE: { + constexpr char sizes[][11] = { "repe scasb", "repe scasw", "repe scasd", "?" }; + return sizes[static_cast(size)]; + } + case Operation::SCAS_REPNE: { + constexpr char sizes[][12] = { "repne scasb", "repne scasw", "repne scasd", "?" }; + return sizes[static_cast(size)]; + } + + case Operation::LODS: { + constexpr char sizes[][6] = { "lodsb", "lodsw", "lodsd", "?" }; + return sizes[static_cast(size)]; + } + case Operation::LODS_REP: { + constexpr char sizes[][10] = { "rep lodsb", "rep lodsw", "rep lodsd", "?" }; + return sizes[static_cast(size)]; + } + + case Operation::MOVS: { + constexpr char sizes[][6] = { "movsb", "movsw", "movsd", "?" }; + return sizes[static_cast(size)]; + } + case Operation::MOVS_REP: { + constexpr char sizes[][10] = { "rep movsb", "rep movsw", "rep movsd", "?" }; + return sizes[static_cast(size)]; + } + + case Operation::STOS: { + constexpr char sizes[][6] = { "stosb", "stosw", "stosd", "?" }; + return sizes[static_cast(size)]; + } + case Operation::STOS_REP: { + constexpr char sizes[][10] = { "rep stosb", "rep stosw", "rep stosd", "?" }; + return sizes[static_cast(size)]; + } + + case Operation::LOOP: return "loop"; + case Operation::LOOPE: return "loope"; + case Operation::LOOPNE: return "loopne"; + + case Operation::MOV: return "mov"; + case Operation::NEG: return "neg"; + case Operation::NOT: return "not"; + case Operation::AND: return "and"; + case Operation::OR: return "or"; + case Operation::XOR: return "xor"; + case Operation::NOP: return "nop"; + case Operation::POP: return "pop"; + case Operation::POPF: return "popf"; + case Operation::PUSH: return "push"; + case Operation::PUSHF: return "pushf"; + case Operation::RCL: return "rcl"; + case Operation::RCR: return "rcr"; + case Operation::ROL: return "rol"; + case Operation::ROR: return "ror"; + case Operation::SAL: return "sal"; + case Operation::SAR: return "sar"; + case Operation::SHR: return "shr"; + + case Operation::CLC: return "clc"; + case Operation::CLD: return "cld"; + case Operation::CLI: return "cli"; + case Operation::STC: return "stc"; + case Operation::STD: return "std"; + case Operation::STI: return "sti"; + case Operation::CMC: return "cmc"; + + case Operation::CMP: return "cmp"; + case Operation::TEST: return "test"; + + case Operation::XCHG: return "xchg"; + case Operation::XLAT: return "xlat"; + case Operation::SALC: return "salc"; + + case Operation::SETMO: + if(model == Model::i8086) { + return "setmo"; + } else { + return "enter"; + } + + case Operation::SETMOC: + if(model == Model::i8086) { + return "setmoc"; + } else { + return "bound"; + } + + case Operation::Invalid: return "invalid"; + + default: + assert(false); + return ""; + } +} + +bool InstructionSet::x86::mnemonic_implies_data_size(Operation operation) { + switch(operation) { + default: return false; + + case Operation::CMPS: + case Operation::LODS: + case Operation::MOVS: + case Operation::SCAS: + case Operation::STOS: + case Operation::JMPrel: + case Operation::LEA: + return true; + } +} + +std::string InstructionSet::x86::to_string(DataSize size) { + constexpr char sizes[][6] = { "byte", "word", "dword", "?" }; + return sizes[static_cast(size)]; +} + +std::string InstructionSet::x86::to_string(Source source, DataSize size) { + switch(source) { + case Source::eAX: { + constexpr char sizes[][4] = { "al", "ax", "eax", "?" }; + return sizes[static_cast(size)]; + } + case Source::eCX: { + constexpr char sizes[][4] = { "cl", "cx", "ecx", "?" }; + return sizes[static_cast(size)]; + } + case Source::eDX: { + constexpr char sizes[][4] = { "dl", "dx", "edx", "?" }; + return sizes[static_cast(size)]; + } + case Source::eBX: { + constexpr char sizes[][4] = { "bl", "bx", "ebx", "?" }; + return sizes[static_cast(size)]; + } + case Source::eSPorAH: { + constexpr char sizes[][4] = { "ah", "sp", "esp", "?" }; + return sizes[static_cast(size)]; + } + case Source::eBPorCH: { + constexpr char sizes[][4] = { "ch", "bp", "ebp", "?" }; + return sizes[static_cast(size)]; + } + case Source::eSIorDH: { + constexpr char sizes[][4] = { "dh", "si", "esi", "?" }; + return sizes[static_cast(size)]; + } + case Source::eDIorBH: { + constexpr char sizes[][4] = { "bh", "di", "edi", "?" }; + return sizes[static_cast(size)]; + } + + case Source::ES: return "es"; + case Source::CS: return "cs"; + case Source::SS: return "ss"; + case Source::DS: return "ds"; + case Source::FS: return "fd"; + case Source::GS: return "gs"; + + case Source::None: return "0"; + case Source::DirectAddress: return "DirectAccess"; + case Source::Immediate: return "Immediate"; + case Source::Indirect: return "Indirect"; + case Source::IndirectNoBase: return "IndirectNoBase"; + + default: return "???"; + } +} + +namespace { + +std::string to_hex(int value, int digits, bool with_suffix = true) { + auto stream = std::stringstream(); + stream << std::setfill('0') << std::uppercase << std::hex << std::setw(digits); + switch(digits) { + case 2: stream << +uint8_t(value); break; + case 4: stream << +uint16_t(value); break; + default: stream << value; break; + } + if (with_suffix) stream << 'h'; + return stream.str(); +}; + +template +std::string to_hex(IntT value) { + auto stream = std::stringstream(); + stream << std::uppercase << std::hex << +value << 'h'; + return stream.str(); +}; + +} + +template +std::string InstructionSet::x86::to_string( + DataPointer pointer, + Instruction instruction, + int offset_length, + int immediate_length, + DataSize operation_size +) { + if(operation_size == InstructionSet::x86::DataSize::None) operation_size = instruction.operation_size(); + + std::string operand; + + auto append = [](std::stringstream &stream, auto value, int length) { + switch(length) { + case 0: + if(!value) { + return; + } + [[fallthrough]]; + + case 2: + value &= 0xff; + break; + } + + stream << std::uppercase << std::hex << value << 'h'; + }; + + auto append_signed = [](std::stringstream &stream, auto value, int length) { + if(!value && !length) { + return; + } + + const bool is_negative = Numeric::top_bit() & value; + const uint64_t abs_value = uint64_t(std::abs(int16_t(value))); // TODO: don't assume 16-bit. + + stream << (is_negative ? '-' : '+') << std::uppercase << std::hex << abs_value << 'h'; + }; + + using Source = InstructionSet::x86::Source; + const Source source = pointer.source(); + switch(source) { + // to_string handles all direct register names correctly. + default: return InstructionSet::x86::to_string(source, operation_size); + + case Source::Immediate: { + std::stringstream stream; + append(stream, instruction.operand(), immediate_length); + return stream.str(); + } + + case Source::DirectAddress: + case Source::Indirect: + case Source::IndirectNoBase: { + std::stringstream stream; + + if(!InstructionSet::x86::mnemonic_implies_data_size(instruction.operation())) { + stream << InstructionSet::x86::to_string(operation_size) << ' '; + } + + stream << '['; + stream << InstructionSet::x86::to_string(instruction.data_segment(), InstructionSet::x86::DataSize::None) << ':'; + + bool addOffset = false; + switch(source) { + default: break; + case Source::Indirect: + stream << InstructionSet::x86::to_string(pointer.base(), data_size(instruction.address_size())); + if(pointer.index() != Source::None) { + stream << '+' << InstructionSet::x86::to_string(pointer.index(), data_size(instruction.address_size())); + } + addOffset = true; + break; + case Source::IndirectNoBase: + stream << InstructionSet::x86::to_string(pointer.index(), data_size(instruction.address_size())); + addOffset = true; + break; + case Source::DirectAddress: + stream << std::uppercase << std::hex << instruction.offset() << 'h'; + break; + } + if(addOffset) { + append_signed(stream, instruction.offset(), offset_length); + } + stream << ']'; + return stream.str(); + } + } + + return operand; +}; + +template +std::string InstructionSet::x86::to_string( + std::pair> instruction, + Model model, + int offset_length, + int immediate_length +) { + std::string operation; + + // Add segment override, if any, ahead of some operations that won't otherwise print it. + switch(instruction.second.operation()) { + default: break; + + case Operation::CMPS: + case Operation::CMPS_REPE: + case Operation::CMPS_REPNE: + case Operation::SCAS: + case Operation::SCAS_REPE: + case Operation::SCAS_REPNE: + case Operation::STOS: + case Operation::STOS_REP: + case Operation::LODS: + case Operation::LODS_REP: + case Operation::MOVS: + case Operation::MOVS_REP: + case Operation::INS: + case Operation::INS_REP: + case Operation::OUTS: + case Operation::OUTS_REP: + switch(instruction.second.data_segment()) { + default: break; + case Source::ES: operation += "es "; break; + case Source::CS: operation += "cs "; break; + case Source::DS: operation += "ds "; break; + case Source::SS: operation += "ss "; break; + case Source::GS: operation += "gs "; break; + case Source::FS: operation += "fs "; break; + } + break; + } + + // Add operation itself. + operation += to_string(instruction.second.operation(), instruction.second.operation_size(), model); + operation += " "; + + // Deal with a few special cases up front. + switch(instruction.second.operation()) { + default: { + const int operands = max_displayed_operands(instruction.second.operation()); + const bool displacement = has_displacement(instruction.second.operation()); + const bool print_first = + instruction.second.destination().source() != Source::None && + ( + operands > 1 || + (operands > 0 && instruction.second.source().source() == Source::None) + ); + if(print_first) { + operation += to_string(instruction.second.destination(), instruction.second, offset_length, immediate_length); + } + if(operands > 0 && instruction.second.source().source() != Source::None) { + if(print_first) operation += ", "; + operation += to_string(instruction.second.source(), instruction.second, offset_length, immediate_length); + } + if(displacement) { + operation += to_hex(instruction.second.displacement() + instruction.first, offset_length); + } + } break; + + case Operation::CALLfar: + case Operation::JMPfar: { + switch(instruction.second.destination().source()) { + case Source::Immediate: + operation += to_hex(instruction.second.segment(), 4, false); + operation += "h:"; + operation += to_hex(instruction.second.offset(), 4, false); + operation += "h"; + break; + default: + operation += to_string(instruction.second.destination(), instruction.second, offset_length, immediate_length); + break; + } + } break; + + case Operation::LDS: + case Operation::LES: // The test set labels the pointer type as dword, which I guess is technically accurate. + // A full 32 bits will be loaded from that address in 16-bit mode. + operation += to_string(instruction.second.destination(), instruction.second, offset_length, immediate_length); + operation += ", "; + operation += to_string(instruction.second.source(), instruction.second, offset_length, immediate_length, InstructionSet::x86::DataSize::DWord); + break; + + case Operation::IN: + operation += to_string(instruction.second.destination(), instruction.second, offset_length, immediate_length); + operation += ", "; + switch(instruction.second.source().source()) { + case Source::DirectAddress: + operation += to_hex(uint8_t(instruction.second.offset())); + break; + default: + operation += to_string(instruction.second.source(), instruction.second, offset_length, immediate_length, InstructionSet::x86::DataSize::Word); + break; + } + break; + + case Operation::OUT: + switch(instruction.second.destination().source()) { + case Source::DirectAddress: + operation += to_hex(uint8_t(instruction.second.offset())); + break; + default: + operation += to_string(instruction.second.destination(), instruction.second, offset_length, immediate_length, InstructionSet::x86::DataSize::Word); + break; + } + operation += ", "; + operation += to_string(instruction.second.source(), instruction.second, offset_length, immediate_length); + break; + + // Rolls and shifts list eCX as a source on the understanding that everyone knows that rolls and shifts + // use CL even when they're shifting or rolling a word-sized quantity. + case Operation::RCL: case Operation::RCR: + case Operation::ROL: case Operation::ROR: + case Operation::SAL: case Operation::SAR: + case Operation::SHR: + case Operation::SETMO: case Operation::SETMOC: + operation += to_string(instruction.second.destination(), instruction.second, offset_length, immediate_length); + switch(instruction.second.source().source()) { + case Source::None: break; + case Source::eCX: operation += ", cl"; break; + case Source::Immediate: + // Providing an immediate operand of 1 is a little future-proofing by the decoder; the '1' + // is actually implicit on a real 8088. So omit it. + if(instruction.second.operand() == 1) break; + [[fallthrough]]; + default: + operation += ", "; + operation += to_string(instruction.second.source(), instruction.second, offset_length, immediate_length); + break; + } + break; + } + + return operation; +} + +// Although advertised, 32-bit printing is incomplete. +// +//template std::string InstructionSet::x86::to_string( +// Instruction instruction, +// Model model, +// int offset_length, +// int immediate_length +//); + +template std::string InstructionSet::x86::to_string( + std::pair> instruction, + Model model, + int offset_length, + int immediate_length +); diff --git a/InstructionSets/x86/Instruction.hpp b/InstructionSets/x86/Instruction.hpp index d34acb44c..a1e475152 100644 --- a/InstructionSets/x86/Instruction.hpp +++ b/InstructionSets/x86/Instruction.hpp @@ -6,15 +6,16 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef InstructionSets_x86_Instruction_h -#define InstructionSets_x86_Instruction_h +#pragma once + +#include "Model.hpp" #include #include +#include #include -namespace InstructionSet { -namespace x86 { +namespace InstructionSet::x86 { /* Operations are documented below to establish expectations as to which @@ -67,11 +68,11 @@ enum class Operation: uint8_t { SBB, /// Subtract; source, destination, operand and displacement will be populated appropriately. SUB, - /// Unsigned multiply; multiplies the source value by AX or AL, storing the result in DX:AX or AX. + /// Unsigned multiply; multiplies the source value by EAX, AX or AL, storing the result in EDX:EAX, DX:AX or AX. MUL, - /// Single operand signed multiply; multiplies the source value by AX or AL, storing the result in DX:AX or AX. + /// Single operand signed multiply; multiplies the source value by EAX, AX or AL, storing the result in EDX:EAX, DX:AX or AX. IMUL_1, - /// Unsigned divide; divide the source value by AX or AL, storing the quotient in AL and the remainder in AH. + /// Unsigned divide; divide the AX, DX:AX or EDX:AX by the source(), storing the quotient in AL, AX or EAX and the remainder in AH, DX or EDX. DIV, /// Signed divide; divide the source value by AX or AL, storing the quotient in AL and the remainder in AH. IDIV, @@ -83,19 +84,19 @@ enum class Operation: uint8_t { /// Reads from the port specified by source to the destination. IN, - /// Writes from the port specified by destination from the source. + /// Writes to the port specified by destination from the source. OUT, // Various jumps; see the displacement to calculate targets. - JO, JNO, JB, JNB, JE, JNE, JBE, JNBE, + JO, JNO, JB, JNB, JZ, JNZ, JBE, JNBE, JS, JNS, JP, JNP, JL, JNL, JLE, JNLE, - /// Far call; see the segment() and offset() fields. - CALLfar, - /// Relative call; see displacement(). - CALLrel, /// Near call. CALLabs, + /// Relative call; see displacement(). + CALLrel, + /// Far call; if destination is Source::Immediate then see the segment() and offset() fields; otherwise take segment and offset by indirection. + CALLfar, /// Return from interrupt. IRET, /// Near return; if source is not ::None then it will be an ::Immediate indicating how many additional bytes to remove from the stack. @@ -106,10 +107,10 @@ enum class Operation: uint8_t { JMPabs, /// Near jump with a relative destination. JMPrel, - /// Far jump to the indicated segment and offset. + /// Far jump; if destination is Source::Immediate then see the segment() and offset() fields; otherwise take segment and offset by indirection. JMPfar, /// Relative jump performed only if CX = 0; see the displacement. - JPCX, + JCXZ, /// Generates a software interrupt of the level stated in the operand. INT, /// Generates a software interrupt of level 4 if overflow is set. @@ -126,25 +127,32 @@ enum class Operation: uint8_t { /// Computes the effective address of the source and loads it into the destination. LEA, - /// Compare [bytes or words, per operation size]; source and destination implied to be DS:[SI] and ES:[DI]. - CMPS, - /// Load string; reads from DS:SI into AL or AX, subject to segment override. - LODS, /// Move string; moves a byte or word from DS:SI to ES:DI. If a segment override is provided, it overrides the the source. MOVS, - /// Scan string; reads a byte or word from DS:SI and compares it to AL or AX. - SCAS, + MOVS_REP, + /// Load string; reads from DS:SI into AL or AX, subject to segment override. + LODS, + LODS_REP, /// Store string; store AL or AX to ES:DI. STOS, + STOS_REP, + /// Compare [bytes or words, per operation size]; source and destination implied to be DS:[SI] and ES:[DI]. + CMPS, + CMPS_REPE, + CMPS_REPNE, + /// Scan string; reads a byte or word from DS:SI and compares it to AL or AX. + SCAS, + SCAS_REPE, + SCAS_REPNE, // Perform a possibly-conditional loop, decrementing CX. See the displacement. LOOP, LOOPE, LOOPNE, /// Loads the destination with the source. MOV, - /// Negatives; source and destination point to the same thing, to negative. + /// Negatives; source indicates what to negative. NEG, - /// Logical NOT; source and destination point to the same thing, to negative. + /// Logical NOT; source indicates what to negative. NOT, /// Logical AND; source, destination, operand and displacement will be populated appropriately. AND, @@ -162,19 +170,27 @@ enum class Operation: uint8_t { PUSH, /// PUSH the flags register to the stack. PUSHF, + /// Rotate the destination left through carry the number of bits indicated by source; if the source is a register then implicitly its size is 1. + /// If it is ::None then the rotation is by a single position only. RCL, /// Rotate the destination right through carry the number of bits indicated by source; if the source is a register then implicitly its size is 1. + /// If it is ::None then the rotation is by a single position only. RCR, /// Rotate the destination left the number of bits indicated by source; if the source is a register then implicitly its size is 1. + /// If it is ::None then the rotation is by a single position only. ROL, /// Rotate the destination right the number of bits indicated by source; if the source is a register then implicitly its size is 1. + /// If it is ::None then the rotation is by a single position only. ROR, /// Arithmetic shift left the destination by the number of bits indicated by source; if the source is a register then implicitly its size is 1. + /// If it is ::None then the shift is by a single position only. SAL, /// Arithmetic shift right the destination by the number of bits indicated by source; if the source is a register then implicitly its size is 1. + /// If it is ::None then the shift is by a single position only. SAR, /// Logical shift right the destination by the number of bits indicated by source; if the source is a register then implicitly its size is 1. + /// If it is ::None then the shift is by a single position only. SHR, /// Clear carry flag; no source or destination provided. @@ -185,7 +201,7 @@ enum class Operation: uint8_t { CLI, /// Set carry flag. STC, - /// Set decimal flag. + /// Set direction flag. STD, /// Set interrupt flag. STI, @@ -203,19 +219,33 @@ enum class Operation: uint8_t { /// Load AL with DS:[AL+BX]. XLAT, + /// Set AL to FFh if carry is set; 00h otherwise. + SALC, + + // + // 8086 exclusives. + // + + /// Set destination to ~0 if CL is non-zero. + SETMOC, + /// Set destination to ~0. + SETMO, + /// Perform an IDIV and negative the result. + IDIV_REP, + // // 80186 additions. // /// Checks whether the signed value in the destination register is within the bounds /// stored at the location indicated by the source register, which will point to two - /// 16- or 32-bit words, the first being a signed lower bound and the signed upper. + /// 16- or 32-bit words, the first being a signed lower bound and the second + /// a signed upper. /// Raises a bounds exception if not. - BOUND, + BOUND = SETMOC, - - /// Create stack frame. See operand() for the nesting level and offset() - /// for the dynamic storage size. + /// Create stack frame. See the Instruction getters `nesting_level()` + /// and `dynamic_storage_size()`. ENTER, /// Procedure exit; copies BP to SP, then pops a new BP from the stack. LEAVE, @@ -224,9 +254,11 @@ enum class Operation: uint8_t { /// ES:[e]DI and incrementing or decrementing [e]DI as per the /// current EFLAGS DF flag. INS, - /// Outputs a byte, word or double word from ES:[e]DI to the port specified by DX, - /// incrementing or decrementing [e]DI as per the current EFLAGS DF flag.] + INS_REP, + /// Outputs a byte, word or double word from ES:[e]DI to the port specified by DX, + /// incrementing or decrementing [e]DI as per the current EFLAGS DF flag. OUTS, + OUTS_REP, /// Pushes all general purpose registers to the stack, in the order: /// AX, CX, DX, BX, [original] SP, BP, SI, DI. @@ -239,42 +271,70 @@ enum class Operation: uint8_t { // 80286 additions. // - // TODO: expand detail on all operations below. - - /// Adjusts requested privilege level. + /// Read a segment selector from the destination and one from the source. + /// If the destination RPL is less than the source, set ZF and set the destination RPL to the source. + /// Otherwise clear ZF and don't modify the destination selector. ARPL, - /// Clears the task-switched flag. + /// Clears the task-switched flag in CR0. CLTS, /// Loads access rights. LAR, - /// Loads the global descriptor table. + /// Loads the global descriptor table register from the source. + /// 32-bit operand: read a 16-bit limit followed by a 32-bit base. + /// 16-bit operand: read a 16-bit limit followed by a 24-bit base. LGDT, - /// Loads the interrupt descriptor table. - LIDT, - /// Loads the local descriptor table. - LLDT, - /// Stores the global descriptor table. + /// Stores the global descriptor table register at the destination; + /// Always stores a 16-bit limit followed by a 32-bit base though + /// the highest byte may be zero. SGDT, - /// Stores the interrupt descriptor table. + + /// Loads the interrupt descriptor table register from the source. + /// 32-bit operand: read a 16-bit limit followed by a 32-bit base. + /// 16-bit operand: read a 16-bit limit followed by a 24-bit base. + LIDT, + /// Stores the interrupt descriptor table register at the destination. + /// Always stores a 16-bit limit followed by a 32-bit base though + /// the highest byte may be zero. SIDT, - /// Stores the local descriptor table. + + /// Loads the local descriptor table register. + /// The source will contain a segment selector pointing into the local descriptor table. + /// That selector is loaded into the local descriptor table register, along with the corresponding + /// segment limit and base from the global descriptor table. + LLDT, + /// Stores the local descriptor table register. SLDT, - /// Verifies a segment for reading. + /// Verifies the segment indicated by source for reading, setting ZF. + /// + /// ZF is set if: (i) the selector is not null; (ii) the selector is within GDT or LDT bounds; + /// (iii) the selector points to code or data; (iv) the segment is readable; + /// (v) the segment is either a conforming code segment, or the segment's DPL + /// is greater than or equal to both the CPL and the selector's RPL. + /// + /// Otherwise ZF is clear. VERR, - /// Verifies a segment for writing. + /// Verifies a segment for writing. Operates as per VERR but checks for writeability + /// rather than readability. VERW, - /// Loads the machine status word. + /// Loads a 16-bit value from source into the machine status word. + /// The low order four bits of the source are copied into CR0, with the caveat + /// that if PE is set, the processor will enter protected mode, but if PE is clear + /// then there will be no change in protected mode. + /// + /// Usurped in function by MOV CR0 as of the 80286. LMSW, - /// Stores the machine status word. + /// Stores the machine status word, i.e. copies the low 16 bits of CR0 into the destination. SMSW, - /// Loads a segment limit + + /// Load the segment limit from descriptor specified by source into destination, + /// setting ZF if successful. LSL, - /// Loads the task register. + /// Load the source operand into the segment selector field of the task register. LTR, - /// Stores the task register. + /// Store the segment seleector of the task register into the destination. STR, /// Three-operand form of IMUL; multiply the immediate by the source and write to the destination. @@ -287,6 +347,8 @@ enum class Operation: uint8_t { // 80386 additions. // + // TODO: expand detail on all operations below. + /// Loads a pointer to FS. LFS, /// Loads a pointer to GS. @@ -340,6 +402,7 @@ enum class Operation: uint8_t { MOVtoTr, MOVfromTr, }; + enum class DataSize: uint8_t { Byte = 0, Word = 1, @@ -347,6 +410,10 @@ enum class DataSize: uint8_t { None = 3, }; +template struct DataSizeType { using type = uint8_t; }; +template <> struct DataSizeType { using type = uint16_t; }; +template <> struct DataSizeType { using type = uint32_t; }; + constexpr int byte_size(DataSize size) { return (1 << int(size)) & 7; } @@ -360,6 +427,9 @@ enum class AddressSize: uint8_t { b32 = 1, }; +template struct AddressSizeType { using type = uint16_t; }; +template <> struct AddressSizeType { using type = uint32_t; }; + constexpr DataSize data_size(AddressSize size) { return DataSize(int(size) + 1); } @@ -403,7 +473,7 @@ enum class Source: uint8_t { T0 = 0, T1 = 1, T2 = 2, T3 = 3, T4 = 4, T5 = 5, T6 = 6, T7 = 7, D0 = 0, D1 = 1, D2 = 2, D3 = 3, D4 = 4, D5 = 5, D6 = 6, D7 = 7, - // Selectors. + // Segment registers. ES, CS, SS, DS, FS, GS, /// @c None can be treated as a source that produces 0 when encountered; @@ -429,11 +499,58 @@ enum class Source: uint8_t { /// getter is used). IndirectNoBase = Indirect - 1, }; +constexpr bool is_register(Source source) { + return source < Source::None; +} +constexpr bool is_segment_register(Source source) { + return is_register(source) && source >= Source::ES; +} enum class Repetition: uint8_t { - None, RepE, RepNE + None, RepE, RepNE, Rep, }; +/// @returns @c true if @c operation supports repetition mode @c repetition; @c false otherwise. +template +constexpr Operation rep_operation(Operation operation, Repetition repetition) { + switch(operation) { + case Operation::IDIV: + if constexpr (model == Model::i8086) { + return repetition != Repetition::None ? Operation::IDIV_REP : Operation::IDIV; + } + [[fallthrough]]; + + default: return operation; + + case Operation::INS: + return repetition != Repetition::None ? Operation::INS_REP : Operation::INS; + case Operation::OUTS: + return repetition != Repetition::None ? Operation::OUTS_REP : Operation::OUTS; + case Operation::LODS: + return repetition != Repetition::None ? Operation::LODS_REP : Operation::LODS; + case Operation::MOVS: + return repetition != Repetition::None ? Operation::MOVS_REP : Operation::MOVS; + case Operation::STOS: + return repetition != Repetition::None ? Operation::STOS_REP : Operation::STOS; + + case Operation::CMPS: + switch(repetition) { + case Repetition::None: return Operation::CMPS; + default: + case Repetition::RepE: return Operation::CMPS_REPE; + case Repetition::RepNE: return Operation::CMPS_REPNE; + } + + case Operation::SCAS: + switch(repetition) { + case Repetition::None: return Operation::SCAS; + default: + case Repetition::RepE: return Operation::SCAS_REPE; + case Repetition::RepNE: return Operation::SCAS_REPNE; + } + } +} + /// Provides a 32-bit-style scale, index and base; to produce the address this represents, /// calcluate base() + (index() << scale()). /// @@ -452,7 +569,7 @@ class ScaleIndexBase { constexpr ScaleIndexBase(int scale, Source index, Source base) noexcept : sib_(uint8_t( scale << 6 | - (int(index != Source::None ? index : Source::eSI) << 3) | + (int(index != Source::None ? index : Source::eSP) << 3) | int(base) )) {} constexpr ScaleIndexBase(Source index, Source base) noexcept : ScaleIndexBase(0, index, base) {} @@ -542,10 +659,7 @@ class DataPointer { ); } - template constexpr Source source() const { - if constexpr (obscure_indirectNoBase) { - return (source_ >= Source::IndirectNoBase) ? Source::Indirect : source_; - } + constexpr Source source() const { return source_; } @@ -557,10 +671,24 @@ class DataPointer { return sib_.index(); } - template constexpr Source base() const { - if constexpr (obscure_indirectNoBase) { - return (source_ <= Source::IndirectNoBase) ? Source::None : sib_.base(); + /// @returns The default segment to use for this access. + constexpr Source default_segment() const { + switch(source_) { + default: + case Source::IndirectNoBase: + return Source::None; + + case Source::Indirect: + switch(base()) { + default: return Source::DS; + case Source::eBP: + case Source::eSP: return Source::SS; + case Source::eDI: return Source::ES; + } } + } + + constexpr Source base() const { return sib_.base(); } @@ -571,10 +699,152 @@ class DataPointer { template class Instruction { public: - Operation operation = Operation::Invalid; + using DisplacementT = typename std::conditional::type; + using ImmediateT = typename std::conditional::type; + using AddressT = ImmediateT; - bool operator ==(const Instruction &rhs) const { - if( operation != rhs.operation || + constexpr Instruction() noexcept {} + constexpr Instruction(Operation operation) noexcept : + Instruction(operation, Source::None, Source::None, ScaleIndexBase(), false, AddressSize::b16, Source::None, DataSize::None, 0, 0) {} + constexpr Instruction( + Operation operation, + Source source, + Source destination, + ScaleIndexBase sib, + bool lock, + AddressSize address_size, + Source segment_override, + DataSize data_size, + DisplacementT displacement, + ImmediateT operand) noexcept : + operation_(operation), + mem_exts_source_(uint8_t( + (int(address_size) << 7) | + (displacement ? 0x40 : 0x00) | + (operand ? 0x20 : 0x00) | + int(source) | + (source == Source::Indirect ? (uint8_t(sib) & 7) : 0) + )), + source_data_dest_sib_(uint16_t( + (int(data_size) << 14) | + (lock ? (1 << 13) : 0) | + ((uint8_t(sib) & 0xf8) << 2) | + int(destination) | + (destination == Source::Indirect ? (uint8_t(sib) & 7) : 0) + )) { + // Decisions on whether to include operand, displacement and/or size extension words + // have implicitly been made in the int packing above; honour them here. + int extension = 0; + if(has_operand()) { + extensions_[extension] = operand; + ++extension; + } + if(has_displacement()) { + extensions_[extension] = ImmediateT(displacement); + ++extension; + } + + // Patch in a fully-resolved segment. + Source segment = segment_override; + if(segment == Source::None) segment = this->source().default_segment(); + if(segment == Source::None) segment = this->destination().default_segment(); + if(segment == Source::None) segment = Source::DS; + source_data_dest_sib_ |= (int(segment)&7) << 10; + } + + /// @returns The number of bytes used for meaningful content within this class. A receiver must use at least @c sizeof(Instruction) bytes + /// to store an @c Instruction but is permitted to reuse the trailing sizeof(Instruction) - packing_size() for any purpose it likes. Teleologically, + /// this allows a denser packing of instructions into containers. + constexpr size_t packing_size() const { + return + offsetof(Instruction, extensions_) + + (has_displacement() + has_operand()) * sizeof(ImmediateT); + } + + /// @returns The @c Operation performed by this instruction. + constexpr Operation operation() const { + return operation_; + } + + /// @returns A @c DataPointer describing the 'destination' of this instruction, conventionally the first operand in Intel-syntax assembly. + constexpr DataPointer destination() const { + return DataPointer( + Source(source_data_dest_sib_ & sib_masks[(source_data_dest_sib_ >> 3) & 3]), + ((source_data_dest_sib_ >> 2) & 0xf8) | (source_data_dest_sib_ & 0x07) + ); + } + + /// @returns A @c DataPointer describing the 'source' of this instruction, conventionally the second operand in Intel-syntax assembly. + constexpr DataPointer source() const { + return DataPointer( + Source(mem_exts_source_ & sib_masks[(mem_exts_source_ >> 3) & 3]), + ((source_data_dest_sib_ >> 2) & 0xf8) | (mem_exts_source_ & 0x07) + ); + } + + /// @returns @c true if the lock prefix was present on this instruction; @c false otherwise. + constexpr bool lock() const { + return source_data_dest_sib_ & (1 << 13); + } + + /// @returns The address size for this instruction; will always be 16-bit for instructions decoded by a 16-bit decoder but can be 16- or 32-bit for + /// instructions decoded by a 32-bit decoder, depending on the program's use of the address size prefix byte. + constexpr AddressSize address_size() const { + return AddressSize(mem_exts_source_ >> 7); + } + + /// @returns The segment that should be used for data fetches if this operation accepts segment overrides. + constexpr Source data_segment() const { + return Source( + int(Source::ES) + + ((source_data_dest_sib_ >> 10) & 7) + ); + } + + /// @returns The data size of this operation — e.g. `MOV AX, BX` has a data size of `::Word` but `MOV EAX, EBX` has a data size of + /// `::DWord`. This value is guaranteed never to be `DataSize::None` even for operations such as `CLI` that don't have operands and operate + /// on data that is not a byte, word or double word. + constexpr DataSize operation_size() const { + return DataSize(source_data_dest_sib_ >> 14); + } + + /// @returns The immediate value provided with this instruction, if any. E.g. `ADD AX, 23h` has the operand `23h`. + constexpr ImmediateT operand() const { + const ImmediateT ops[] = {0, operand_extension()}; + return ops[has_operand()]; + } + + /// @returns The nesting level argument supplied to an ENTER. + constexpr ImmediateT nesting_level() const { + return operand(); + } + + /// @returns The immediate segment value provided with this instruction, if any. Relevant for far calls and jumps; e.g. `JMP 1234h:5678h` will + /// have a segment value of `1234h`. + constexpr uint16_t segment() const { + return uint16_t(operand()); + } + + /// @returns The offset provided with this instruction, if any. E.g. `MOV AX, [es:1998h]` has an offset of `1998h`. + constexpr ImmediateT offset() const { + const ImmediateT offsets[] = {0, displacement_extension()}; + return offsets[has_displacement()]; + } + + /// @returns The displacement provided with this instruction `SUB AX, [SI+BP-23h]` has an offset of `-23h` and `JMP 19h` + /// has an offset of `19h`. + constexpr DisplacementT displacement() const { + return DisplacementT(offset()); + } + + /// @returns The dynamic storage size argument supplied to an ENTER. + constexpr ImmediateT dynamic_storage_size() const { + return displacement(); + } + + // Standard comparison operator. + constexpr bool operator ==(const Instruction &rhs) const { + if( operation_ != rhs.operation_ || mem_exts_source_ != rhs.mem_exts_source_ || source_data_dest_sib_ != rhs.source_data_dest_sib_) { return false; @@ -582,7 +852,7 @@ template class Instruction { // Have already established above that this and RHS have the // same extensions, if any. - const int extension_count = has_length_extension() + has_displacement() + has_operand(); + const int extension_count = has_displacement() + has_operand(); for(int c = 0; c < extension_count; c++) { if(extensions_[c] != rhs.extensions_[c]) return false; } @@ -590,21 +860,17 @@ template class Instruction { return true; } - using DisplacementT = typename std::conditional::type; - using ImmediateT = typename std::conditional::type; - using AddressT = ImmediateT; - private: + Operation operation_ = Operation::Invalid; + // Packing and encoding of fields is admittedly somewhat convoluted; what this // achieves is that instructions will be sized: // - // four bytes + up to three extension words - // (two bytes for 16-bit instructions, four for 32) + // four bytes + up to two extension words + // (extension words being two bytes for 16-bit instructions, four for 32) // - // Two of the extension words are used to retain an operand and displacement - // if the instruction has those. The other can store sizes greater than 15 - // bytes (for earlier processors), plus any repetition, segment override or - // repetition prefixes. + // The extension words are used to retain an operand and displacement + // if the instruction has those. // b7: address size; // b6: has displacement; @@ -620,24 +886,14 @@ template class Instruction { } // [b15, b14]: data size; - // [b13, b10]: source length (0 => has length extension); + // [b13]: lock; + // [b12, b10]: segment override; // [b9, b5]: top five of SIB; // [b4, b0]: dest. - uint16_t source_data_dest_sib_ = 1 << 10; // So that ::Invalid doesn't seem to have a length extension. + uint16_t source_data_dest_sib_ = 0; - bool has_length_extension() const { - return !((source_data_dest_sib_ >> 10) & 15); - } - - // {operand}, {displacement}, {length extension}. - // - // If length extension is present then: - // - // [b15, b6]: source length; - // [b5, b4]: repetition; - // [b3, b1]: segment override; - // b0: lock. - ImmediateT extensions_[3]{}; + // {operand}, {displacement}. + ImmediateT extensions_[2]{}; ImmediateT operand_extension() const { return extensions_[0]; @@ -645,158 +901,66 @@ template class Instruction { ImmediateT displacement_extension() const { return extensions_[(mem_exts_source_ >> 5) & 1]; } - ImmediateT length_extension() const { - return extensions_[((mem_exts_source_ >> 5) & 1) + ((mem_exts_source_ >> 6) & 1)]; - } - public: - /// @returns The number of bytes used for meaningful content within this class. A receiver must use at least @c sizeof(Instruction) bytes - /// to store an @c Instruction but is permitted to reuse the trailing sizeof(Instruction) - packing_size() for any purpose it likes. Teleologically, - /// this allows a denser packing of instructions into containers. - size_t packing_size() const { - return - offsetof(Instruction, extensions) + - (has_displacement() + has_operand() + has_length_extension()) * sizeof(ImmediateT); - - // To consider in the future: the length extension is always the last one, - // and uses only 8 bits of content within 32-bit instructions, so it'd be - // possible further to trim the packing size on little endian machines. - // - // ... but is that a speed improvement? How much space does it save, and - // is it enough to undo the costs of unaligned data? - } - - private: // A lookup table to help with stripping parts of the SIB that have been // hidden within the source/destination fields. static constexpr uint8_t sib_masks[] = { 0x1f, 0x1f, 0x1f, 0x18 }; - public: - DataPointer source() const { - return DataPointer( - Source(mem_exts_source_ & sib_masks[(mem_exts_source_ >> 3) & 3]), - ((source_data_dest_sib_ >> 2) & 0xf8) | (mem_exts_source_ & 0x07) - ); - } - DataPointer destination() const { - return DataPointer( - Source(source_data_dest_sib_ & sib_masks[(source_data_dest_sib_ >> 3) & 3]), - ((source_data_dest_sib_ >> 2) & 0xf8) | (source_data_dest_sib_ & 0x07) - ); - } - bool lock() const { - return has_length_extension() && length_extension()&1; - } - - AddressSize address_size() const { - return AddressSize(mem_exts_source_ >> 7); - } - - /// @returns @c Source::DS if no segment override was found; the overridden segment otherwise. - /// On x86 a segment override cannot modify the segment used as a destination in string instructions, - /// or that used by stack instructions, but this function does not spend the time necessary to provide - /// the correct default for those. - Source data_segment() const { - if(!has_length_extension()) return Source::DS; - return Source( - int(Source::ES) + - ((length_extension() >> 1) & 7) - ); - } - - Repetition repetition() const { - if(!has_length_extension()) return Repetition::None; - return Repetition((length_extension() >> 4) & 3); - } - DataSize operation_size() const { - return DataSize(source_data_dest_sib_ >> 14); - } - - int length() const { - const int short_length = (source_data_dest_sib_ >> 10) & 15; - if(short_length) return short_length; - return length_extension() >> 6; - } - - ImmediateT operand() const { - const ImmediateT ops[] = {0, operand_extension()}; - return ops[has_operand()]; - } - DisplacementT displacement() const { - return DisplacementT(offset()); - } - - uint16_t segment() const { - return uint16_t(operand()); - } - ImmediateT offset() const { - const ImmediateT offsets[] = {0, displacement_extension()}; - return offsets[has_displacement()]; - } - - constexpr Instruction() noexcept {} - constexpr Instruction(Operation operation, int length) noexcept : - Instruction(operation, Source::None, Source::None, ScaleIndexBase(), false, AddressSize::b16, Source::None, Repetition::None, DataSize::None, 0, 0, length) {} - constexpr Instruction( - Operation operation, - Source source, - Source destination, - ScaleIndexBase sib, - bool lock, - AddressSize address_size, - Source segment_override, - Repetition repetition, - DataSize data_size, - DisplacementT displacement, - ImmediateT operand, - int length) noexcept : - operation(operation), - mem_exts_source_(uint8_t( - (int(address_size) << 7) | - (displacement ? 0x40 : 0x00) | - (operand ? 0x20 : 0x00) | - int(source) | - (source == Source::Indirect ? (uint8_t(sib) & 7) : 0) - )), - source_data_dest_sib_(uint16_t( - (int(data_size) << 14) | - (( - (lock || (segment_override != Source::None) || (length > 15) || (repetition != Repetition::None)) - ) ? 0 : (length << 10)) | - ((uint8_t(sib) & 0xf8) << 2) | - int(destination) | - (destination == Source::Indirect ? (uint8_t(sib) & 7) : 0) - )) { - - // Decisions on whether to include operand, displacement and/or size extension words - // have implicitly been made in the int packing above; honour them here. - int extension = 0; - if(has_operand()) { - extensions_[extension] = operand; - ++extension; - } - if(has_displacement()) { - extensions_[extension] = ImmediateT(displacement); - ++extension; - } - if(has_length_extension()) { - // As per the rule stated for segment(), this class provides ::DS for any instruction - // that doesn't have a segment override. - if(segment_override == Source::None) segment_override = Source::DS; - extensions_[extension] = ImmediateT( - (length << 6) | (int(repetition) << 4) | ((int(segment_override) & 7) << 1) | int(lock) - ); - ++extension; - } - } }; static_assert(sizeof(Instruction) <= 16); static_assert(sizeof(Instruction) <= 10); -} -} +// +// Disassembly aids. +// -#endif /* InstructionSets_x86_Instruction_h */ +/// @returns @c true if @c operation uses a @c displacement(). +bool has_displacement(Operation operation); + +/// @returns The maximum number of operands to print in a disassembly of @c operation; +/// i.e. 2 for both source() and destination(), 1 for source() alone, 0 for neither. This is a maximum +/// only — if either source is Source::None then it should not be printed. +int max_displayed_operands(Operation operation); + +/// Provides the idiomatic name of the @c Operation given an operation @c DataSize and processor @c Model. +std::string to_string(Operation, DataSize, Model); + +/// @returns @c true if the idiomatic name of @c Operation implies the data size (e.g. stosb), @c false otherwise (e.g. ld). +bool mnemonic_implies_data_size(Operation); + +/// Provides the name of the @c DataSize, i.e. 'byte', 'word' or 'dword'. +std::string to_string(DataSize); + +/// Provides the name of the @c Source at @c DataSize, e.g. for Source::eAX it might return AL, AX or EAX. +std::string to_string(Source, DataSize); + +/// Provides the printable version of @c pointer as an appendage for @c instruction. +/// +/// See notes below re: @c offset_length and @c immediate_length. +/// If @c operation_size is the default value of @c ::None, it'll be taken from the @c instruction. +template +std::string to_string( + DataPointer pointer, + Instruction instruction, + int offset_length, + int immediate_length, + DataSize operation_size = InstructionSet::x86::DataSize::None +); + +/// Provides the printable version of @c instruction. +/// +/// Internally, instructions do not retain the original sizes of offsets/displacements or immediates so the following are available: +/// +/// If @c offset_length is '2' or '4', truncates any printed offset to 2 or 4 digits if it is compatible with being that length. +/// If @c immediate_length is '2' or '4', truncates any printed immediate value to 2 or 4 digits if it is compatible with being that length. +template +std::string to_string( + std::pair> instruction, + Model model, + int offset_length = 0, + int immediate_length = 0); + +} diff --git a/InstructionSets/x86/Interrupts.hpp b/InstructionSets/x86/Interrupts.hpp new file mode 100644 index 000000000..acefa35f9 --- /dev/null +++ b/InstructionSets/x86/Interrupts.hpp @@ -0,0 +1,36 @@ +// +// Interrupts.hpp +// Clock Signal +// +// Created by Thomas Harte on 06/10/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +namespace InstructionSet::x86 { + +enum Interrupt { + DivideError = 0, + SingleStep = 1, + NMI = 2, + Breakpoint = 3, + Overflow = 4, + BoundRangeExceeded = 5, + InvalidOpcode = 6, + DeviceNotAvailable = 7, + DoubleFault = 8, + CoprocessorSegmentOverrun = 9, + InvalidTSS = 10, + SegmentNotPresent = 11, + StackSegmentFault = 12, + GeneralProtectionFault = 13, + PageFault = 14, + /* 15 is reserved */ + FloatingPointException = 16, + AlignmentCheck = 17, + MachineCheck = 18, + +}; + +} diff --git a/InstructionSets/x86/Model.hpp b/InstructionSets/x86/Model.hpp index 304214475..8bd8ea8d8 100644 --- a/InstructionSets/x86/Model.hpp +++ b/InstructionSets/x86/Model.hpp @@ -6,11 +6,11 @@ // Copyright © 2022 Thomas Harte. All rights reserved. // -#ifndef Model_h -#define Model_h +#pragma once -namespace InstructionSet { -namespace x86 { +#include + +namespace InstructionSet::x86 { enum class Model { i8086, @@ -21,7 +21,7 @@ enum class Model { static constexpr bool is_32bit(Model model) { return model >= Model::i80386; } -} -} +template struct AddressT { using type = uint16_t; }; +template <> struct AddressT { using type = uint32_t; }; -#endif /* Model_h */ +} diff --git a/InstructionSets/x86/Perform.hpp b/InstructionSets/x86/Perform.hpp new file mode 100644 index 000000000..46821aab7 --- /dev/null +++ b/InstructionSets/x86/Perform.hpp @@ -0,0 +1,53 @@ +// +// Perform.hpp +// Clock Signal +// +// Created by Thomas Harte on 05/10/2023. +// Copyright © 2023 Thomas Harte. All rights reserved. +// + +#pragma once + +#include "Instruction.hpp" +#include "Model.hpp" +#include "Flags.hpp" + +namespace InstructionSet::x86 { + +template < + Model model_, + typename FlowControllerT, + typename RegistersT, + typename MemoryT, + typename IOT +> struct ExecutionContext { + FlowControllerT flow_controller; + Flags flags; + RegistersT registers; + MemoryT memory; + IOT io; + static constexpr Model model = model_; +}; + +/// Performs @c instruction querying @c registers and/or @c memory as required, using @c io for port input/output, +/// and providing any flow control effects to @c flow_controller. +/// +/// Any change in processor status will be applied to @c status. +template < + typename InstructionT, + typename ContextT +> void perform( + const InstructionT &instruction, + ContextT &context +); + +template < + typename ContextT +> void interrupt( + int index, + ContextT &context +); + +} + +#include "Implementation/PerformImplementation.hpp" diff --git a/Machines/Amiga/Amiga.cpp b/Machines/Amiga/Amiga.cpp index 370217d7c..acce943d3 100644 --- a/Machines/Amiga/Amiga.cpp +++ b/Machines/Amiga/Amiga.cpp @@ -11,15 +11,13 @@ #include "../../Activity/Source.hpp" #include "../MachineTypes.hpp" -#include "../../Processors/68000Mk2/68000Mk2.hpp" +#include "../../Processors/68000/68000.hpp" #include "../../Analyser/Static/Amiga/Target.hpp" #include "../Utility/MemoryPacker.hpp" #include "../Utility/MemoryFuzzer.hpp" -//#define NDEBUG -#define LOG_PREFIX "[Amiga] " #include "../../Outputs/Log.hpp" #include "Chipset.hpp" @@ -35,13 +33,15 @@ namespace { constexpr int PALClockRate = 7'093'790; //constexpr int NTSCClockRate = 7'159'090; +Log::Logger logger; + } namespace Amiga { class ConcreteMachine: public Activity::Source, - public CPU::MC68000Mk2::BusHandler, + public CPU::MC68000::BusHandler, public MachineTypes::AudioProducer, public MachineTypes::JoystickMachine, public MachineTypes::MappedKeyboardMachine, @@ -81,10 +81,9 @@ class ConcreteMachine: // MARK: - MC68000::BusHandler. template HalfCycles perform_bus_operation(const Microcycle &cycle, int) { - // Do a quick advance check for Chip RAM access; add a suitable delay if required. HalfCycles total_length; - if(cycle.operation & Microcycle::NewAddress && *cycle.address < 0x20'0000) { + if(cycle.operation & CPU::MC68000::Operation::NewAddress && *cycle.address < 0x20'0000) { total_length = chipset_.run_until_after_cpu_slot().duration; assert(total_length >= cycle.length); } else { @@ -94,19 +93,19 @@ class ConcreteMachine: mc68000_.set_interrupt_level(chipset_.get_interrupt_level()); // Check for assertion of reset. - if(cycle.operation & Microcycle::Reset) { + if(cycle.operation & CPU::MC68000::Operation::Reset) { memory_.reset(); - LOG("Reset; PC is around " << PADHEX(8) << mc68000_.get_state().registers.program_counter); + logger.info().append("Reset; PC is around %08x", mc68000_.get_state().registers.program_counter); } // Autovector interrupts. - if(cycle.operation & Microcycle::InterruptAcknowledge) { + if(cycle.operation & CPU::MC68000::Operation::InterruptAcknowledge) { mc68000_.set_is_peripheral_address(true); return total_length - cycle.length; } // Do nothing if no address is exposed. - if(!(cycle.operation & (Microcycle::NewAddress | Microcycle::SameAddress))) return total_length - cycle.length; + if(!(cycle.operation & (CPU::MC68000::Operation::NewAddress | CPU::MC68000::Operation::SameAddress))) return total_length - cycle.length; // Grab the target address to pick a memory source. const uint32_t address = cycle.host_endian_byte_address(); @@ -115,7 +114,7 @@ class ConcreteMachine: mc68000_.set_is_peripheral_address((address & 0xe0'0000) == 0xa0'0000); if(!memory_.regions[address >> 18].read_write_mask) { - if((cycle.operation & (Microcycle::SelectByte | Microcycle::SelectWord))) { + if((cycle.operation & (CPU::MC68000::Operation::SelectByte | CPU::MC68000::Operation::SelectWord))) { // Check for various potential chip accesses. // Per the manual: @@ -133,7 +132,7 @@ class ConcreteMachine: const bool select_a = !(address & 0x1000); const bool select_b = !(address & 0x2000); - if(cycle.operation & Microcycle::Read) { + if(cycle.operation & CPU::MC68000::Operation::Read) { uint16_t result = 0xffff; if(select_a) result &= 0xff00 | (chipset_.cia_a.read(reg) << 0); if(select_b) result &= 0x00ff | (chipset_.cia_b.read(reg) << 8); @@ -143,7 +142,7 @@ class ConcreteMachine: if(select_b) chipset_.cia_b.write(reg, cycle.value8_high()); } -// LOG("CIA " << (((address >> 12) & 3)^3) << " " << (cycle.operation & Microcycle::Read ? "read " : "write ") << std::dec << (reg & 0xf) << " of " << PADHEX(4) << +cycle.value16()); +// logger.info().append("CIA %d %s %d of %04x", ((address >> 12) & 3)^3, operation & Microcycle::Read ? "read" : "write", reg & 0xf, cycle.value16()); } else if(address >= 0xdf'f000 && address <= 0xdf'f1be) { chipset_.perform(cycle); } else if(address >= 0xe8'0000 && address < 0xe9'0000) { @@ -155,13 +154,13 @@ class ConcreteMachine: memory_.perform(cycle); } else { // This'll do for open bus, for now. - if(cycle.operation & Microcycle::Read) { + if(cycle.operation & CPU::MC68000::Operation::Read) { cycle.set_value16(0xffff); } - // Don't log for the region that is definitely just ROM this machine doesn't have. + // Log only for the region that is definitely not just ROM this machine doesn't have. if(address < 0xf0'0000) { - LOG("Unmapped " << (cycle.operation & Microcycle::Read ? "read from " : "write to ") << PADHEX(6) << ((*cycle.address)&0xffffff) << " of " << cycle.value16()); + logger.error().append("Unmapped %s %06x of %04x", cycle.operation & CPU::MC68000::Operation::Read ? "read from " : "write to ", (*cycle.address)&0xffffff, cycle.value16()); } } } @@ -177,7 +176,7 @@ class ConcreteMachine: } private: - CPU::MC68000Mk2::Processor mc68000_; + CPU::MC68000::Processor mc68000_; // MARK: - Memory map. @@ -186,7 +185,7 @@ class ConcreteMachine: // MARK: - Chipset. Chipset chipset_; - + // MARK: - Activity Source void set_activity_observer(Activity::Observer *observer) final { @@ -252,10 +251,10 @@ class ConcreteMachine: using namespace Amiga; -Machine *Machine::Amiga(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { +std::unique_ptr Machine::Amiga(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { using Target = Analyser::Static::Amiga::Target; const Target *const amiga_target = dynamic_cast(target); - return new Amiga::ConcreteMachine(*amiga_target, rom_fetcher); + return std::make_unique(*amiga_target, rom_fetcher); } Machine::~Machine() {} diff --git a/Machines/Amiga/Amiga.hpp b/Machines/Amiga/Amiga.hpp index caa7fe965..f0b6cd4d5 100644 --- a/Machines/Amiga/Amiga.hpp +++ b/Machines/Amiga/Amiga.hpp @@ -6,12 +6,13 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef Amiga_hpp -#define Amiga_hpp +#pragma once #include "../../Analyser/Static/StaticAnalyser.hpp" #include "../ROMMachine.hpp" +#include + namespace Amiga { class Machine { @@ -19,9 +20,7 @@ class Machine { virtual ~Machine(); /// Creates and returns an Amiga. - static Machine *Amiga(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); + static std::unique_ptr Amiga(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); }; } - -#endif /* Amiga_hpp */ diff --git a/Machines/Amiga/Audio.cpp b/Machines/Amiga/Audio.cpp index 94b963da6..d300e1ab9 100644 --- a/Machines/Amiga/Audio.cpp +++ b/Machines/Amiga/Audio.cpp @@ -10,9 +10,6 @@ #include "Flags.hpp" -#define LOG_PREFIX "[Audio] " -#include "../../Outputs/Log.hpp" - #include #include diff --git a/Machines/Amiga/Audio.hpp b/Machines/Amiga/Audio.hpp index 78681d8e8..1f73faba8 100644 --- a/Machines/Amiga/Audio.hpp +++ b/Machines/Amiga/Audio.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef Audio_hpp -#define Audio_hpp +#pragma once #include #include @@ -159,5 +158,3 @@ class Audio: public DMADevice<4> { }; } - -#endif /* Audio_hpp */ diff --git a/Machines/Amiga/Bitplanes.hpp b/Machines/Amiga/Bitplanes.hpp index 62664621c..6c6fad3dc 100644 --- a/Machines/Amiga/Bitplanes.hpp +++ b/Machines/Amiga/Bitplanes.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef Bitplanes_hpp -#define Bitplanes_hpp +#pragma once #include @@ -46,7 +45,7 @@ class Bitplanes: public DMADevice<6, 2> { BitplaneData next; }; -template constexpr SourceT bitplane_swizzle(SourceT value) { +template constexpr SourceT bitplane_swizzle(SourceT value) { return (value&0x21) | ((value&0x02) << 2) | @@ -78,9 +77,9 @@ class BitplaneShifter { /// The value is arranges so that MSB = first pixel to output, LSB = last. /// /// Each byte is swizzled to provide easier playfield separation, being in the form: - /// b6, b7 = 0; - /// b3–b5: planes 1, 3 and 5; - /// b0–b2: planes 0, 2 and 4. + /// b6, b7 = 0; + /// b3–b5: planes 1, 3 and 5; + /// b0–b2: planes 0, 2 and 4. uint32_t get(bool high_res) { if(high_res) { return uint32_t(data_[1] >> 32); @@ -98,5 +97,3 @@ class BitplaneShifter { }; } - -#endif /* Bitplanes_hpp */ diff --git a/Machines/Amiga/Blitter.cpp b/Machines/Amiga/Blitter.cpp index 3eae3c7fc..e384fe20a 100644 --- a/Machines/Amiga/Blitter.cpp +++ b/Machines/Amiga/Blitter.cpp @@ -9,20 +9,16 @@ #include "Blitter.hpp" #include "Minterms.hpp" +#include "../../Outputs/Log.hpp" #include -#ifndef NDEBUG -#define NDEBUG -#endif - -#define LOG_PREFIX "[Blitter] " -#include "../../Outputs/Log.hpp" - using namespace Amiga; namespace { +Log::Logger logger; + /// @returns Either the final carry flag or the output nibble when using fill mode given that it either @c is_exclusive fill mode, or isn't; /// and the specified initial @c carry and input @c nibble. template constexpr uint32_t fill_nibble(bool is_exclusive, uint8_t carry, uint8_t nibble) { @@ -130,18 +126,18 @@ void Blitter::set_control(int index, uint16_t value) { sequencer_.set_control(value >> 8); } shifts_[index] = value >> 12; - LOG("Set control " << index << " to " << PADHEX(4) << value); + logger.info().append("Set control %d to %04x", index, value); } template void Blitter::set_first_word_mask(uint16_t value) { - LOG("Set first word mask: " << PADHEX(4) << value); + logger.info().append("Set first word mask: %04x", value); a_mask_[0] = value; } template void Blitter::set_last_word_mask(uint16_t value) { - LOG("Set last word mask: " << PADHEX(4) << value); + logger.info().append("Set last word mask: %04x", value); a_mask_[1] = value; } @@ -153,7 +149,7 @@ void Blitter::set_size(uint16_t value) { if(!width_) width_ = 0x40; height_ = value >> 6; if(!height_) height_ = 1024; - LOG("Set size to " << std::dec << width_ << ", " << height_); + logger.info().append("Set size to %d, %d", width_, height_); // Current assumption: writing this register informs the // blitter that it should treat itself as about to start a new line. @@ -161,24 +157,24 @@ void Blitter::set_size(uint16_t value) { template void Blitter::set_minterms(uint16_t value) { - LOG("Set minterms " << PADHEX(4) << value); + logger.info().append("Set minterms: %02x", value & 0xff); minterms_ = value & 0xff; } //template //void Blitter::set_vertical_size([[maybe_unused]] uint16_t value) { -// LOG("Set vertical size " << PADHEX(4) << value); +// logger.info().append("Set vertical size %04x", value); // // TODO. This is ECS only, I think. Ditto set_horizontal_size. //} // //template //void Blitter::set_horizontal_size([[maybe_unused]] uint16_t value) { -// LOG("Set horizontal size " << PADHEX(4) << value); +// logger.info().append("Set horizontal size %04x", value); //} template void Blitter::set_data(int channel, uint16_t value) { - LOG("Set data " << channel << " to " << PADHEX(4) << value); + logger.info().append("Set data %d to %04x", channel, value); // Ugh, backed myself into a corner. TODO: clean. switch(channel) { @@ -193,7 +189,7 @@ template uint16_t Blitter::get_status() { const uint16_t result = (not_zero_flag_ ? 0x0000 : 0x2000) | (height_ ? 0x4000 : 0x0000); - LOG("Returned status of " << result); + logger.info().append("Returned status of %04x", result); return result; } diff --git a/Machines/Amiga/Blitter.hpp b/Machines/Amiga/Blitter.hpp index 2a4ce8ae4..ecd934143 100644 --- a/Machines/Amiga/Blitter.hpp +++ b/Machines/Amiga/Blitter.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef Blitter_hpp -#define Blitter_hpp +#pragma once #include #include @@ -129,6 +128,3 @@ template class Blitter: public DMADevice<4, 4> { }; } - - -#endif /* Blitter_hpp */ diff --git a/Machines/Amiga/BlitterSequencer.hpp b/Machines/Amiga/BlitterSequencer.hpp index c6f0ff642..e6acb0279 100644 --- a/Machines/Amiga/BlitterSequencer.hpp +++ b/Machines/Amiga/BlitterSequencer.hpp @@ -6,8 +6,7 @@ // Copyright © 2022 Thomas Harte. All rights reserved. // -#ifndef BlitterSequencer_hpp -#define BlitterSequencer_hpp +#pragma once #include @@ -163,5 +162,3 @@ class BlitterSequencer { }; } - -#endif /* BlitterSequencer_hpp */ diff --git a/Machines/Amiga/Chipset.cpp b/Machines/Amiga/Chipset.cpp index 8f1c23f25..3a7b0ccc3 100644 --- a/Machines/Amiga/Chipset.cpp +++ b/Machines/Amiga/Chipset.cpp @@ -8,17 +8,16 @@ #include "Chipset.hpp" -#ifndef NDEBUG -#define NDEBUG -#endif - -#define LOG_PREFIX "[Amiga chipset] " #include "../../Outputs/Log.hpp" #include #include #include +namespace { +Log::Logger logger; +} + using namespace Amiga; #define DMA_CONSTRUCT *this, reinterpret_cast(map.chip_ram.data()), map.chip_ram.size() >> 1 @@ -393,9 +392,9 @@ template void Chipset::output() { constexpr int end_of_pixels = 15; constexpr int blank1 = 3 + end_of_pixels; constexpr int sync = 17 + blank1; - constexpr int blank2 = 3 + sync; - constexpr int burst = 9 + blank2; - constexpr int blank3 = 6 + burst; + constexpr int blank2 = 3 + sync; + constexpr int burst = 9 + blank2; + constexpr int blank3 = 6 + burst; static_assert(blank3 == 53); #define LINK(location, action, length) \ @@ -674,8 +673,8 @@ template int Chipset::advance_slots(int first_slot, int last_ } assert(last_slot > first_slot); -#define C(x) \ - case x: \ +#define C(x) \ + case x: \ output(); \ \ if constexpr (stop_on_cpu) { \ @@ -683,8 +682,8 @@ template int Chipset::advance_slots(int first_slot, int last_ return 1 + x - first_slot; \ } \ } else { \ - perform_cycle(); \ - } \ + perform_cycle(); \ + } \ \ if((x + 1) == last_slot) break; \ [[fallthrough]] @@ -841,17 +840,6 @@ void Chipset::update_interrupts() { } } -void Chipset::perform(const CPU::MC68000Mk2::Microcycle &cycle) { - using Microcycle = CPU::MC68000Mk2::Microcycle; - - const uint32_t register_address = *cycle.address & ChipsetAddressMask; - if(cycle.operation & Microcycle::Read) { - cycle.set_value16(read(register_address)); - } else { - write(register_address, cycle.value16()); - } -} - void Chipset::write(uint32_t address, uint16_t value, bool allow_conversion) { #define ApplySetClear(target, mask) { \ if(value & 0x8000) { \ @@ -880,16 +868,16 @@ void Chipset::write(uint32_t address, uint16_t value, bool allow_conversion) { break; case 0x02a: // VPOSW - LOG("TODO: write vertical position high " << PADHEX(4) << value); + logger.error().append("TODO: write vertical position high %04x", value); break; case 0x02c: // VHPOSW - LOG("TODO: write vertical position low " << PADHEX(4) << value); + logger.error().append("TODO: write vertical position low %04x", value); is_long_field_ = value & 0x8000; break; // Joystick/mouse input. case 0x034: // POTGO -// LOG("TODO: pot port start"); +// logger.error().append("TODO: pot port start"); break; // Disk DMA and control. @@ -898,11 +886,11 @@ void Chipset::write(uint32_t address, uint16_t value, bool allow_conversion) { case 0x024: disk_.set_length(value); break; // DSKLEN case 0x026: // DSKDAT - LOG("TODO: disk DMA; " << PADHEX(4) << value << " to " << address); + logger.error().append("TODO: disk DMA; %04x to %04x", value, address); break; case 0x09e: // ADKCON - LOG("Write disk control"); + logger.info().append("Write disk control"); ApplySetClear(paula_disk_control_, 0x7fff); disk_controller_.set_control(paula_disk_control_); @@ -916,7 +904,7 @@ void Chipset::write(uint32_t address, uint16_t value, bool allow_conversion) { // Refresh. case 0x028: // REFPTR - LOG("TODO (maybe): refresh; " << PADHEX(4) << value << " to " << PADHEX(8) << address); + logger.info().append("TODO (maybe): refresh; %04x to %08x", value, address); break; // Serial port. @@ -955,7 +943,7 @@ void Chipset::write(uint32_t address, uint16_t value, bool allow_conversion) { break; case 0x092: // DDFSTRT if(fetch_window_[0] != value) { - LOG("Fetch window start set to " << std::dec << value); + logger.info().append("Fetch window start set to %d", value); } fetch_window_[0] = value & 0xfe; break; @@ -963,7 +951,7 @@ void Chipset::write(uint32_t address, uint16_t value, bool allow_conversion) { // TODO: something in my interpretation of ddfstart and ddfstop // means a + 8 is needed below for high-res displays. Investigate. if(fetch_window_[1] != value) { - LOG("Fetch window stop set to " << std::dec << fetch_window_[1]); + logger.info().append("Fetch window stop set to %d", fetch_window_[1]); } fetch_window_[1] = value & 0xfe; break; @@ -1000,7 +988,7 @@ void Chipset::write(uint32_t address, uint16_t value, bool allow_conversion) { break; case 0x106: // BPLCON3 (ECS) - LOG("TODO: Bitplane control; " << PADHEX(4) << value << " to " << PADHEX(8) << address); + logger.error().append("TODO: Bitplane control; %04x to %08x", value, address); break; case 0x108: bitplanes_.set_modulo<0>(value); break; // BPL1MOD @@ -1012,7 +1000,7 @@ void Chipset::write(uint32_t address, uint16_t value, bool allow_conversion) { case 0x116: case 0x118: case 0x11a: - LOG("TODO: Bitplane data; " << PADHEX(4) << value << " to " << PADHEX(8) << address); + logger.error().append("TODO: Bitplane data; %04x to %08x", value, address); break; // Blitter. @@ -1067,7 +1055,7 @@ void Chipset::write(uint32_t address, uint16_t value, bool allow_conversion) { case 0x088: copper_.reload<0>(); break; case 0x08a: copper_.reload<1>(); break; case 0x08c: - LOG("TODO: coprocessor instruction fetch identity " << PADHEX(4) << value); + logger.error().append("TODO: coprocessor instruction fetch identity %04x", value); break; // Sprites. @@ -1155,15 +1143,15 @@ uint16_t Chipset::read(uint32_t address, bool allow_conversion) { case 0x00c: return joystick(0).get_position(); // JOY1DAT case 0x016: // POTGOR / POTINP -// LOG("TODO: pot port read"); +// logger.error().append("TODO: pot port read"); return 0xff00; // Disk DMA and control. case 0x010: // ADKCONR - LOG("Read disk control"); + logger.info().append("Read disk control"); return paula_disk_control_; case 0x01a: // DSKBYTR - LOG("TODO: disk status"); + logger.error().append("TODO: disk status"); assert(false); // Not yet implemented. return 0xffff; @@ -1205,7 +1193,7 @@ Chipset::CIAAHandler::CIAAHandler(MemoryMap &map, DiskController &controller, Mo void Chipset::CIAAHandler::set_port_output(MOS::MOS6526::Port port, uint8_t value) { if(port) { // CIA A, Port B: Parallel port output. - LOG("TODO: parallel output " << PADHEX(2) << +value); + logger.info().append("TODO: parallel output %02x", value); } else { // CIA A, Port A: // @@ -1227,7 +1215,7 @@ void Chipset::CIAAHandler::set_port_output(MOS::MOS6526::Port port, uint8_t valu uint8_t Chipset::CIAAHandler::get_port_input(MOS::MOS6526::Port port) { if(port) { - LOG("TODO: parallel input?"); + logger.info().append("TODO: parallel input?"); } else { // Use the mouse as FIR0, the joystick as FIR1. return @@ -1267,12 +1255,12 @@ void Chipset::CIABHandler::set_port_output(MOS::MOS6526::Port port, uint8_t valu // b2: SEL // b1: POUT // b0: BUSY - LOG("TODO: DTR/RTS/etc: " << PADHEX(2) << +value); + logger.error().append("TODO: DTR/RTS/etc: %02x", value); } } uint8_t Chipset::CIABHandler::get_port_input(MOS::MOS6526::Port) { - LOG("Unexpected: input for CIA B"); + logger.error().append("Unexpected: input for CIA B"); return 0xff; } diff --git a/Machines/Amiga/Chipset.hpp b/Machines/Amiga/Chipset.hpp index 9957f0328..6bc59f939 100644 --- a/Machines/Amiga/Chipset.hpp +++ b/Machines/Amiga/Chipset.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef Chipset_hpp -#define Chipset_hpp +#pragma once #include #include @@ -20,7 +19,7 @@ #include "../../ClockReceiver/JustInTime.hpp" #include "../../Components/6526/6526.hpp" #include "../../Outputs/CRT/CRT.hpp" -#include "../../Processors/68000Mk2/68000Mk2.hpp" +#include "../../Processors/68000/68000.hpp" #include "../../Storage/Disk/Controller/DiskController.hpp" #include "../../Storage/Disk/Drive.hpp" @@ -58,7 +57,15 @@ class Chipset: private ClockingHint::Observer { Changes run_until_after_cpu_slot(); /// Performs the provided microcycle, which the caller guarantees to be a memory access. - void perform(const CPU::MC68000Mk2::Microcycle &); + template + void perform(const Microcycle &cycle) { + const uint32_t register_address = *cycle.address & ChipsetAddressMask; + if(cycle.operation & CPU::MC68000::Operation::Read) { + cycle.set_value16(read(register_address)); + } else { + write(register_address, cycle.value16()); + } + } /// Sets the current state of the CIA interrupt lines. void set_cia_interrupts(bool cia_a, bool cia_b); @@ -217,9 +224,9 @@ class Chipset: private ClockingHint::Observer { uint16_t get_status(); private: - uint16_t value = 0, reload = 0; - uint16_t shift = 0, receive_shift = 0; - uint16_t status; +// uint16_t value = 0, reload = 0; +// uint16_t shift = 0, receive_shift = 0; +// uint16_t status; } serial_; // MARK: - Pixel output. @@ -368,5 +375,3 @@ class Chipset: private ClockingHint::Observer { }; } - -#endif /* Chipset_hpp */ diff --git a/Machines/Amiga/Copper.cpp b/Machines/Amiga/Copper.cpp index 5db62fc49..ef39af0ba 100644 --- a/Machines/Amiga/Copper.cpp +++ b/Machines/Amiga/Copper.cpp @@ -6,15 +6,10 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef NDEBUG -#define NDEBUG -#endif - -#define LOG_PREFIX "[Copper] " -#include "../../Outputs/Log.hpp" - -#include "Chipset.hpp" #include "Copper.hpp" +#include "Chipset.hpp" + +#include "../../Outputs/Log.hpp" using namespace Amiga; @@ -31,6 +26,8 @@ bool satisfies_raster(uint16_t position, uint16_t blitter_status, uint16_t *inst return (position & mask) >= (instruction[0] & mask); } +Log::Logger logger; + } // @@ -85,7 +82,7 @@ bool Copper::advance_dma(uint16_t position, uint16_t blitter_status) { case State::Waiting: if(satisfies_raster(position, blitter_status, instruction_)) { - LOG("Unblocked waiting for " << PADHEX(4) << instruction_[0] << " at " << PADHEX(4) << position << " with mask " << PADHEX(4) << (instruction_[1] & 0x7ffe)); + logger.info().append("Unblocked waiting for %04x at %04x with mask %04x", instruction_[0], position, instruction_[1] & 0x7ffe); state_ = State::FetchFirstWord; } return false; @@ -94,7 +91,7 @@ bool Copper::advance_dma(uint16_t position, uint16_t blitter_status) { instruction_[0] = ram_[address_ & ram_mask_]; ++address_; state_ = State::FetchSecondWord; - LOG("First word fetch at " << PADHEX(4) << position); + logger.info().append("First word fetch at %04x", position); break; case State::FetchSecondWord: { @@ -105,7 +102,7 @@ bool Copper::advance_dma(uint16_t position, uint16_t blitter_status) { // Read in the second instruction word. instruction_[1] = ram_[address_ & ram_mask_]; ++address_; - LOG("Second word fetch at " << PADHEX(4) << position); + logger.info().append("Second word fetch at %04x", position); // Check for a MOVE. if(!(instruction_[0] & 1)) { @@ -113,7 +110,7 @@ bool Copper::advance_dma(uint16_t position, uint16_t blitter_status) { // Stop if this move would be a privilege violation. instruction_[0] &= 0x1fe; if((instruction_[0] < 0x10) || (instruction_[0] < 0x20 && !(control_&1))) { - LOG("Invalid MOVE to " << PADHEX(4) << instruction_[0] << "; stopping"); + logger.info().append("Invalid MOVE to %04x; stopping", instruction_[0]); state_ = State::Stopped; break; } diff --git a/Machines/Amiga/Copper.hpp b/Machines/Amiga/Copper.hpp index 761d2a6f6..5c16912b5 100644 --- a/Machines/Amiga/Copper.hpp +++ b/Machines/Amiga/Copper.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef Copper_h -#define Copper_h +#pragma once #include "DMADevice.hpp" @@ -50,5 +49,3 @@ class Copper: public DMADevice<2> { }; } - -#endif /* Copper_h */ diff --git a/Machines/Amiga/DMADevice.hpp b/Machines/Amiga/DMADevice.hpp index c3d1d7e0b..a1ecf6426 100644 --- a/Machines/Amiga/DMADevice.hpp +++ b/Machines/Amiga/DMADevice.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef DMADevice_hpp -#define DMADevice_hpp +#pragma once #include #include @@ -70,5 +69,3 @@ template class DMADevice: public }; } - -#endif /* DMADevice_hpp */ diff --git a/Machines/Amiga/Disk.cpp b/Machines/Amiga/Disk.cpp index f2f667899..7a2612a8b 100644 --- a/Machines/Amiga/Disk.cpp +++ b/Machines/Amiga/Disk.cpp @@ -8,13 +8,12 @@ #include "Chipset.hpp" -#ifndef NDEBUG -#define NDEBUG -#endif - -#define LOG_PREFIX "[Disk] " #include "../../Outputs/Log.hpp" +namespace { +Log::Logger logger; +} + using namespace Amiga; // MARK: - Disk DMA. @@ -46,7 +45,7 @@ void Chipset::DiskDMA::set_length(uint16_t value) { buffer_read_ = buffer_write_ = 0; if(dma_enable_) { - LOG("Disk DMA " << (write_ ? "write" : "read") << " of " << length_ << " to " << PADHEX(8) << pointer_[0]); + logger.info().append("Disk DMA %s of %d to %08x", write_ ? "write" : "read", length_, pointer_[0]); } state_ = sync_with_word_ ? State::WaitingForSync : State::Reading; @@ -110,7 +109,7 @@ void Chipset::DiskController::process_input_bit(int value) { } void Chipset::DiskController::set_sync_word(uint16_t value) { - LOG("Set disk sync word to " << PADHEX(4) << value); + logger.info().append("Set disk sync word to %04x", value); sync_word_ = value; } @@ -128,7 +127,7 @@ void Chipset::DiskController::set_control(uint16_t control) { bit_length.clock_rate = (control & 0x100) ? 500000 : 250000; set_expected_bit_length(bit_length); - LOG((sync_with_word_ ? "Will" : "Won't") << " sync with word; bit length is " << ((control & 0x100) ? "short" : "long")); + logger.info().append("%s sync with word; bit length is %s", sync_with_word_ ? "Will" : "Won't", (control & 0x100) ? "short" : "long"); } void Chipset::DiskController::process_index_hole() { @@ -175,14 +174,14 @@ void Chipset::DiskController::set_mtr_sel_side_dir_step(uint8_t value) { const bool is_selected = !(value & select_mask); // Both the motor state and the ID shifter are affected upon - // changes in drive selection only. - if(difference & select_mask) { + // the trailing edge of changes in drive selection only. + if(difference & select_mask & ~value) { // If transitioning to inactive, shift the drive ID value; // if transitioning to active, possibly reset the drive // ID and definitely latch the new motor state. if(!is_selected) { drive_ids_[c] <<= 1; - LOG("Shifted drive ID shift register for drive " << +c << " to " << PADHEX(4) << std::bitset<16>{drive_ids_[c]}); + logger.info().append("Shifted drive ID shift register for drive %d to %08x", c, drive_ids_[c]); } else { // Motor transition on -> off => reload register. if(!motor_on && drive.get_motor_on()) { @@ -191,7 +190,7 @@ void Chipset::DiskController::set_mtr_sel_side_dir_step(uint8_t value) { // 0x5555'5555 = 5.25" drive; // 0x0000'0000 = no drive. drive_ids_[c] = 0xffff'ffff; - LOG("Reloaded drive ID shift register for drive " << +c); + logger.info().append("Reloaded drive ID shift register for drive %d", c); } // Also latch the new motor state. @@ -204,7 +203,7 @@ void Chipset::DiskController::set_mtr_sel_side_dir_step(uint8_t value) { // Possibly step. if(did_step && is_selected) { - LOG("Stepped drive " << +c << " by " << std::dec << +direction.as_int()); + logger.info().append("Stepped drive %d by %d", c, direction.as_int()); drive.step(direction); } } diff --git a/Machines/Amiga/Flags.hpp b/Machines/Amiga/Flags.hpp index 029c1a1d8..280bf2512 100644 --- a/Machines/Amiga/Flags.hpp +++ b/Machines/Amiga/Flags.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef Flags_hpp -#define Flags_hpp +#pragma once namespace Amiga { @@ -49,5 +48,3 @@ namespace DMAFlag { } } - -#endif /* Flags_hpp */ diff --git a/Machines/Amiga/Keyboard.cpp b/Machines/Amiga/Keyboard.cpp index 0b3916328..54f01f137 100644 --- a/Machines/Amiga/Keyboard.cpp +++ b/Machines/Amiga/Keyboard.cpp @@ -12,7 +12,7 @@ // // // Before -// the transmission starts, both KCLK and KDAT are high. The keyboard starts +// the transmission starts, both KCLK and KDAT are high. The keyboard starts // the transmission by putting out the first data bit (on KDAT), followed by // a pulse on KCLK (low then high); then it puts out the second data bit and // pulses KCLK until all eight data bits have been sent. @@ -20,23 +20,23 @@ // When the computer has received the eighth bit, it must pulse KDAT low for // at least 1 (one) microsecond, as a handshake signal to the keyboard. The // keyboard must be able to detect pulses greater than or equal -// to 1 microsecond. Software MUST pulse the line low for 85 microseconds to +// to 1 microsecond. Software MUST pulse the line low for 85 microseconds to // ensure compatibility with all keyboard models. // // // If the handshake pulse does not arrive within // 143 ms of the last clock of the transmission, the keyboard will assume // that the computer is still waiting for the rest of the transmission and is -// therefore out of sync. The keyboard will then attempt to restore sync by -// going into "resync mode." In this mode, the keyboard clocks out a 1 and +// therefore out of sync. The keyboard will then attempt to restore sync by +// going into "resync mode." In this mode, the keyboard clocks out a 1 and // waits for a handshake pulse. If none arrives within 143 ms, it clocks out // another 1 and waits again. // // The keyboard Hard Resets the Amiga by pulling KCLK low and starting a 500 -// millisecond timer. When one or more of the keys is released and 500 +// millisecond timer. When one or more of the keys is released and 500 // milliseconds have passed, the keyboard will release KCLK. // -// The usual sequence of events will therefore be: power-up; synchronize; +// The usual sequence of events will therefore be: power-up; synchronize; // transmit "initiate power-up key stream" ($FD); transmit "terminate key // stream" ($FE). @@ -52,8 +52,8 @@ Keyboard::Keyboard(Serial::Line &output) : output_(output) { switch(shift_state_) { case ShiftState::Shifting: // The keyboard processor sets the KDAT line about 20 microseconds before it - // pulls KCLK low. KCLK stays low for about 20 microseconds, then goes high - // again. The processor waits another 20 microseconds before changing KDAT. + // pulls KCLK low. KCLK stays low for about 20 microseconds, then goes high + // again. The processor waits another 20 microseconds before changing KDAT. switch(bit_phase_) { default: break; case 0: lines_ = Lines::Clock | (shift_sequence_ & 1); break; diff --git a/Machines/Amiga/Keyboard.hpp b/Machines/Amiga/Keyboard.hpp index 31818edd8..95a7aa786 100644 --- a/Machines/Amiga/Keyboard.hpp +++ b/Machines/Amiga/Keyboard.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef Machines_Amiga_Keyboard_hpp -#define Machines_Amiga_Keyboard_hpp +#pragma once #include #include @@ -96,26 +95,24 @@ class Keyboard { } private: - enum class ShiftState { - Shifting, - AwaitingHandshake, - Idle, - } shift_state_ = ShiftState::Idle; +// enum class ShiftState { +// Shifting, +// AwaitingHandshake, +// Idle, +// } shift_state_ = ShiftState::Idle; - enum class State { - Startup, - } state_ = State::Startup; +// enum class State { +// Startup, +// } state_ = State::Startup; - int bit_phase_ = 0; - uint32_t shift_sequence_ = 0; - int bits_remaining_ = 0; +// int bit_phase_ = 0; +// uint32_t shift_sequence_ = 0; +// int bits_remaining_ = 0; - uint8_t lines_ = 0; +// uint8_t lines_ = 0; Serial::Line &output_; std::array pressed_{}; }; } - -#endif /* Machines_Amiga_Keyboard_hpp */ diff --git a/Machines/Amiga/MemoryMap.hpp b/Machines/Amiga/MemoryMap.hpp index fbe920027..d0d2d53bb 100644 --- a/Machines/Amiga/MemoryMap.hpp +++ b/Machines/Amiga/MemoryMap.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef MemoryMap_hpp -#define MemoryMap_hpp +#pragma once #include "../../Analyser/Static/Amiga/Target.hpp" @@ -19,8 +18,8 @@ namespace Amiga { class MemoryMap { private: - static constexpr auto PermitRead = CPU::MC68000Mk2::Microcycle::PermitRead; - static constexpr auto PermitWrite = CPU::MC68000Mk2::Microcycle::PermitWrite; + static constexpr auto PermitRead = CPU::MC68000::Operation::PermitRead; + static constexpr auto PermitWrite = CPU::MC68000::Operation::PermitWrite; static constexpr auto PermitReadWrite = PermitRead | PermitWrite; public: @@ -109,13 +108,13 @@ class MemoryMap { /// Performs the provided microcycle, which the caller guarantees to be a memory access, /// and in the Zorro register range. - bool perform(const CPU::MC68000Mk2::Microcycle &cycle) { + template + bool perform(const Microcycle &cycle) { if(!fast_autoconf_visible_) return false; const uint32_t register_address = *cycle.address & 0xfe; - using Microcycle = CPU::MC68000Mk2::Microcycle; - if(cycle.operation & Microcycle::Read) { + if(cycle.operation & CPU::MC68000::Operation::Read) { // Re: Autoconf: // // "All read registers physically return only the top 4 bits of data, on D31-D28"; @@ -193,4 +192,3 @@ class MemoryMap { }; } -#endif /* MemoryMap_hpp */ diff --git a/Machines/Amiga/Minterms.hpp b/Machines/Amiga/Minterms.hpp index bf92d0b0a..012c8cb01 100644 --- a/Machines/Amiga/Minterms.hpp +++ b/Machines/Amiga/Minterms.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef Minterms_hpp -#define Minterms_hpp +#pragma once namespace Amiga { @@ -383,5 +382,3 @@ template IntT apply_minterm(IntT a, IntT b, IntT c, int minterm) } } - -#endif /* Minterms_hpp */ diff --git a/Machines/Amiga/MouseJoystick.hpp b/Machines/Amiga/MouseJoystick.hpp index 32037fc9c..3289c3764 100644 --- a/Machines/Amiga/MouseJoystick.hpp +++ b/Machines/Amiga/MouseJoystick.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef MouseJoystick_hpp -#define MouseJoystick_hpp +#pragma once #include #include @@ -53,5 +52,3 @@ class Joystick: public Inputs::ConcreteJoystick, public MouseJoystickInput { }; } - -#endif /* MouseJoystick_hpp */ diff --git a/Machines/Amiga/Sprites.hpp b/Machines/Amiga/Sprites.hpp index a2c05da4e..1ce95ed5d 100644 --- a/Machines/Amiga/Sprites.hpp +++ b/Machines/Amiga/Sprites.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef Sprites_hpp -#define Sprites_hpp +#pragma once #include @@ -64,7 +63,4 @@ class TwoSpriteShifter { uint8_t overflow_; }; - } - -#endif /* Sprites_hpp */ diff --git a/Machines/AmstradCPC/AmstradCPC.cpp b/Machines/AmstradCPC/AmstradCPC.cpp index bcffa6f3c..ce15aae9b 100644 --- a/Machines/AmstradCPC/AmstradCPC.cpp +++ b/Machines/AmstradCPC/AmstradCPC.cpp @@ -389,9 +389,7 @@ class CRTCBusHandler { // A black border can be output via crt_.output_blank for a minor performance // win; otherwise paint whatever the border colour really is. if(border_) { - uint8_t *const colour_pointer = static_cast(crt_.begin_data(1)); - if(colour_pointer) *colour_pointer = border_; - crt_.output_level(length * 16); + crt_.output_level(length * 16, border_); } else { crt_.output_blank(length * 16); } @@ -585,6 +583,10 @@ class CRTCBusHandler { InterruptTimer &interrupt_timer_; }; +using CRTC = Motorola::CRTC::CRTC6845< + CRTCBusHandler, + Motorola::CRTC::Personality::HD6845S, + Motorola::CRTC::CursorType::None>; /*! Holds and vends the current keyboard state, acting as the AY's port handler. @@ -683,7 +685,7 @@ class i8255PortHandler : public Intel::i8255::PortHandler { public: i8255PortHandler( KeyboardState &key_state, - const Motorola::CRTC::CRTC6845 &crtc, + const CRTC &crtc, AYDeferrer &ay, Storage::Tape::BinaryTapePlayer &tape_player) : ay_(ay), @@ -742,7 +744,7 @@ class i8255PortHandler : public Intel::i8255::PortHandler { private: AYDeferrer &ay_; - const Motorola::CRTC::CRTC6845 &crtc_; + const CRTC &crtc_; KeyboardState &key_state_; Storage::Tape::BinaryTapePlayer &tape_player_; }; @@ -767,7 +769,7 @@ template class ConcreteMachine: ConcreteMachine(const Analyser::Static::AmstradCPC::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) : z80_(*this), crtc_bus_handler_(ram_, interrupt_timer_), - crtc_(Motorola::CRTC::HD6845S, crtc_bus_handler_), + crtc_(crtc_bus_handler_), i8255_port_handler_(key_state_, crtc_, ay_, tape_player_), i8255_(i8255_port_handler_), tape_player_(8000000), @@ -905,7 +907,7 @@ template class ConcreteMachine: // first bit of this byte. parser.process_pulse(tape_player_.get_current_pulse()); const auto byte = parser.get_byte(tape_player_.get_tape()); - auto flags = z80_.get_value_of_register(CPU::Z80::Register::Flags); + auto flags = z80_.value_of(CPU::Z80::Register::Flags); if(byte) { // In A ROM-esque fashion, begin the first pulse after the final one @@ -927,14 +929,14 @@ template class ConcreteMachine: write_pointers_[(tape_crc_address+1) >> 14][(tape_crc_address+1) & 16383] = uint8_t(crc_value >> 8); // Indicate successful byte read. - z80_.set_value_of_register(CPU::Z80::Register::A, *byte); + z80_.set_value_of(CPU::Z80::Register::A, *byte); flags |= CPU::Z80::Flag::Carry; } else { // TODO: return tape player to previous state and decline to serve. - z80_.set_value_of_register(CPU::Z80::Register::A, 0); + z80_.set_value_of(CPU::Z80::Register::A, 0); flags &= ~CPU::Z80::Flag::Carry; } - z80_.set_value_of_register(CPU::Z80::Register::Flags, flags); + z80_.set_value_of(CPU::Z80::Register::Flags, flags); // RET. *cycle.value = 0xc9; @@ -1219,7 +1221,7 @@ template class ConcreteMachine: CPU::Z80::Processor z80_; CRTCBusHandler crtc_bus_handler_; - Motorola::CRTC::CRTC6845 crtc_; + CRTC crtc_; AYDeferrer ay_; i8255PortHandler i8255_port_handler_; @@ -1283,12 +1285,12 @@ template class ConcreteMachine: using namespace AmstradCPC; // See header; constructs and returns an instance of the Amstrad CPC. -Machine *Machine::AmstradCPC(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { +std::unique_ptr Machine::AmstradCPC(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { using Target = Analyser::Static::AmstradCPC::Target; const Target *const cpc_target = dynamic_cast(target); switch(cpc_target->model) { - default: return new AmstradCPC::ConcreteMachine(*cpc_target, rom_fetcher); - case Target::Model::CPC464: return new AmstradCPC::ConcreteMachine(*cpc_target, rom_fetcher); + default: return std::make_unique>(*cpc_target, rom_fetcher); + case Target::Model::CPC464: return std::make_unique>(*cpc_target, rom_fetcher); } } diff --git a/Machines/AmstradCPC/AmstradCPC.hpp b/Machines/AmstradCPC/AmstradCPC.hpp index ae1cf1015..75cf8af6c 100644 --- a/Machines/AmstradCPC/AmstradCPC.hpp +++ b/Machines/AmstradCPC/AmstradCPC.hpp @@ -6,8 +6,7 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef AmstradCPC_hpp -#define AmstradCPC_hpp +#pragma once #include "../../Configurable/Configurable.hpp" #include "../../Configurable/StandardOptions.hpp" @@ -26,7 +25,7 @@ class Machine { virtual ~Machine(); /// Creates and returns an Amstrad CPC. - static Machine *AmstradCPC(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); + static std::unique_ptr AmstradCPC(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); /// Defines the runtime options available for an Amstrad CPC. class Options: @@ -51,5 +50,3 @@ class Machine { }; } - -#endif /* AmstradCPC_hpp */ diff --git a/Machines/AmstradCPC/FDC.hpp b/Machines/AmstradCPC/FDC.hpp index 46042e0f5..4cf2424c7 100644 --- a/Machines/AmstradCPC/FDC.hpp +++ b/Machines/AmstradCPC/FDC.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef FDC_h -#define FDC_h +#pragma once #include "../../Components/8272/i8272.hpp" @@ -47,5 +46,3 @@ class FDC: public Intel::i8272::i8272 { }; } - -#endif /* FDC_h */ diff --git a/Machines/AmstradCPC/Keyboard.hpp b/Machines/AmstradCPC/Keyboard.hpp index c5913637e..864f5e933 100644 --- a/Machines/AmstradCPC/Keyboard.hpp +++ b/Machines/AmstradCPC/Keyboard.hpp @@ -6,8 +6,7 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef Machines_AmstradCPC_Keyboard_hpp -#define Machines_AmstradCPC_Keyboard_hpp +#pragma once #include "../KeyboardMachine.hpp" #include "../Utility/Typer.hpp" @@ -44,6 +43,4 @@ struct CharacterMapper: public ::Utility::CharacterMapper { bool needs_pause_after_key(uint16_t key) const override; }; -}; - -#endif /* KeyboardMapper_hpp */ +} diff --git a/Machines/Apple/ADB/Bus.hpp b/Machines/Apple/ADB/Bus.hpp index a39f530ef..89cc02c83 100644 --- a/Machines/Apple/ADB/Bus.hpp +++ b/Machines/Apple/ADB/Bus.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef Bus_hpp -#define Bus_hpp +#pragma once #include "../../../ClockReceiver/ClockReceiver.hpp" @@ -16,8 +15,7 @@ #include #include -namespace Apple { -namespace ADB { +namespace Apple::ADB { struct Command { enum class Type { @@ -48,7 +46,7 @@ inline std::ostream &operator <<(std::ostream &stream, Command::Type type) { case Command::Type::Flush: stream << "flush"; break; case Command::Type::Listen: stream << "listen"; break; case Command::Type::Talk: stream << "talk"; break; - default: stream << "reserved"; break; + default: stream << "reserved"; break; } return stream; } @@ -167,6 +165,3 @@ class Bus { }; } -} - -#endif /* Bus_hpp */ diff --git a/Machines/Apple/ADB/Keyboard.cpp b/Machines/Apple/ADB/Keyboard.cpp index 9a9ad556f..5eaf2619c 100644 --- a/Machines/Apple/ADB/Keyboard.cpp +++ b/Machines/Apple/ADB/Keyboard.cpp @@ -72,8 +72,13 @@ void Keyboard::did_receive_data(const Command &, const std::vector &dat bool Keyboard::set_key_pressed(Key key, bool is_pressed) { - // ADB keyboard events: low 7 bits are a key code; bit 7 is either 0 for pressed or 1 for released. std::lock_guard lock_guard(keys_mutex_); + if(pressed_keys_[size_t(key)] == is_pressed) { + return true; + } + + // ADB keyboard events: low 7 bits are a key code; + // bit 7 is either 0 for pressed or 1 for released. pending_events_.push_back(uint8_t(key) | (is_pressed ? 0x00 : 0x80)); pressed_keys_[size_t(key)] = is_pressed; @@ -161,7 +166,7 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) const { switch(key) { default: return MachineTypes::MappedKeyboardMachine::KeyNotMapped; -#define Bind(x, y) case Key::x: return uint16_t(ADBKey::y) +#define Bind(x, y) case Key::x: return uint16_t(ADBKey::y) #define BindDirect(x) Bind(x, x) BindDirect(BackTick); diff --git a/Machines/Apple/ADB/Keyboard.hpp b/Machines/Apple/ADB/Keyboard.hpp index a8213dd28..97a50d35f 100644 --- a/Machines/Apple/ADB/Keyboard.hpp +++ b/Machines/Apple/ADB/Keyboard.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef Keyboard_hpp -#define Keyboard_hpp +#pragma once #include "ReactiveDevice.hpp" #include "../../../Inputs/Keyboard.hpp" @@ -18,8 +17,7 @@ #include #include -namespace Apple { -namespace ADB { +namespace Apple::ADB { /*! Defines the keycodes that could be passed directly via set_key_pressed; these @@ -107,7 +105,7 @@ class Keyboard: public ReactiveDevice { void did_receive_data(const Command &, const std::vector &) override; std::mutex keys_mutex_; - std::array pressed_keys_; + std::array pressed_keys_{}; std::vector pending_events_; uint16_t modifiers_ = 0xffff; }; @@ -120,6 +118,3 @@ class KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper }; } -} - -#endif /* Keyboard_hpp */ diff --git a/Machines/Apple/ADB/Mouse.cpp b/Machines/Apple/ADB/Mouse.cpp index ba0a5310f..a514611b4 100644 --- a/Machines/Apple/ADB/Mouse.cpp +++ b/Machines/Apple/ADB/Mouse.cpp @@ -15,24 +15,36 @@ using namespace Apple::ADB; Mouse::Mouse(Bus &bus) : ReactiveDevice(bus, 3) {} void Mouse::perform_command(const Command &command) { - if(command.type == Command::Type::Talk && command.reg == 0) { - // Read current deltas and buttons, thread safely. - auto delta_x = delta_x_.exchange(0); - auto delta_y = delta_y_.exchange(0); - const int buttons = button_flags_; + // Mouse deltas are confined to a seven-bit signed field; this implementation keeps things symmetrical by + // limiting them to a maximum absolute value of 63 in any direction. + static constexpr int16_t max_delta = 63; - // Clamp deltas. - delta_x = std::clamp(delta_x, int16_t(-128), int16_t(127)); - delta_y = std::clamp(delta_y, int16_t(-128), int16_t(127)); + if(command.type == Command::Type::Talk && command.reg == 0) { + // Read and clamp current deltas and buttons. + // + // There's some small chance of creating negative feedback here — taking too much off delta_x_ or delta_y_ + // due to a change in the underlying value between the load and the arithmetic. But if that occurs it means + // the user moved the mouse again in the interim, so it'll just play out as very slight latency. + auto delta_x = delta_x_.load(std::memory_order::memory_order_relaxed); + auto delta_y = delta_y_.load(std::memory_order::memory_order_relaxed); + if(abs(delta_x) > max_delta || abs(delta_y) > max_delta) { + int max = std::max(abs(delta_x), abs(delta_y)); + delta_x = delta_x * max_delta / max; + delta_y = delta_y * max_delta / max; + } + + const int buttons = button_flags_.load(std::memory_order::memory_order_relaxed); + delta_x_ -= delta_x; + delta_y_ -= delta_y; // Figure out what that would look like, and don't respond if there's - // no change to report. + // no change or deltas to report. const uint16_t reg0 = ((buttons & 1) ? 0x0000 : 0x8000) | ((buttons & 2) ? 0x0000 : 0x0080) | uint16_t(delta_x & 0x7f) | uint16_t((delta_y & 0x7f) << 8); - if(reg0 == last_posted_reg0_) return; + if(!(reg0 & 0x7f7f) && (reg0 & 0x8080) == (last_posted_reg0_ & 0x8080)) return; // Post change. last_posted_reg0_ = reg0; diff --git a/Machines/Apple/ADB/Mouse.hpp b/Machines/Apple/ADB/Mouse.hpp index 3e8a4dc1f..2835552d5 100644 --- a/Machines/Apple/ADB/Mouse.hpp +++ b/Machines/Apple/ADB/Mouse.hpp @@ -6,14 +6,12 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef Mouse_hpp -#define Mouse_hpp +#pragma once #include "ReactiveDevice.hpp" #include "../../../Inputs/Mouse.hpp" -namespace Apple { -namespace ADB { +namespace Apple::ADB { class Mouse: public ReactiveDevice, public Inputs::Mouse { public: @@ -33,6 +31,3 @@ class Mouse: public ReactiveDevice, public Inputs::Mouse { }; } -} - -#endif /* Mouse_hpp */ diff --git a/Machines/Apple/ADB/ReactiveDevice.cpp b/Machines/Apple/ADB/ReactiveDevice.cpp index d8bbc4436..1ba80f462 100644 --- a/Machines/Apple/ADB/ReactiveDevice.cpp +++ b/Machines/Apple/ADB/ReactiveDevice.cpp @@ -8,11 +8,14 @@ #include "ReactiveDevice.hpp" -#define LOG_PREFIX "[ADB device] " #include "../../../Outputs/Log.hpp" using namespace Apple::ADB; +namespace { +Log::Logger logger; +} + ReactiveDevice::ReactiveDevice(Apple::ADB::Bus &bus, uint8_t adb_device_id) : bus_(bus), device_id_(bus.add_device(this)), @@ -125,7 +128,7 @@ void ReactiveDevice::adb_bus_did_observe_event(Bus::Event event, uint8_t value) phase_ = Phase::AwaitingAttention; command_ = decode_command(value); -// LOG(command_); +// logger.info().append("%d", command_); // If this command doesn't apply here, but a service request is requested, // post a service request. diff --git a/Machines/Apple/ADB/ReactiveDevice.hpp b/Machines/Apple/ADB/ReactiveDevice.hpp index 54a3ad53c..58b165163 100644 --- a/Machines/Apple/ADB/ReactiveDevice.hpp +++ b/Machines/Apple/ADB/ReactiveDevice.hpp @@ -6,8 +6,7 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef ReactiveDevice_hpp -#define ReactiveDevice_hpp +#pragma once #include "Bus.hpp" @@ -15,8 +14,7 @@ #include #include -namespace Apple { -namespace ADB { +namespace Apple::ADB { class ReactiveDevice: public Bus::Device { protected: @@ -61,6 +59,3 @@ class ReactiveDevice: public Bus::Device { }; } -} - -#endif /* ReactiveDevice_hpp */ diff --git a/Machines/Apple/AppleII/AppleII.cpp b/Machines/Apple/AppleII/AppleII.cpp index d99f31147..dace81906 100644 --- a/Machines/Apple/AppleII/AppleII.cpp +++ b/Machines/Apple/AppleII/AppleII.cpp @@ -243,8 +243,8 @@ template class ConcreteMachine: page(0xc1, 0xc4, state.region_C1_C3 ? &rom_[0xc100 - 0xc100] : nullptr, nullptr); read_pages_[0xc3] = state.region_C3 ? &rom_[0xc300 - 0xc100] : nullptr; - page(0xc4, 0xc8, state.region_C4_C8 ? &rom_[0xc400 - 0xc100] : nullptr, nullptr); - page(0xc8, 0xd0, state.region_C8_D0 ? &rom_[0xc800 - 0xc100] : nullptr, nullptr); + page(0xc4, 0xc8, state.region_C4_C8 ? &rom_[0xc400 - 0xc100] : nullptr, nullptr); + page(0xc8, 0xd0, state.region_C8_D0 ? &rom_[0xc800 - 0xc100] : nullptr, nullptr); } if constexpr (bool(type & PagingType::Main)) { @@ -276,10 +276,20 @@ template class ConcreteMachine: Keyboard(Processor *m6502) : m6502_(m6502) {} void reset_all_keys() final { - open_apple_is_pressed = closed_apple_is_pressed = control_is_pressed = shift_is_pressed = key_is_down = false; + open_apple_is_pressed = + closed_apple_is_pressed = + control_is_pressed_ = + shift_is_pressed_ = + repeat_is_pressed_ = + key_is_down_ = + character_is_pressed_ = false; } - bool set_key_pressed(Key key, char value, bool is_pressed) final { + bool set_key_pressed(Key key, char value, bool is_pressed, bool is_repeat) final { + if constexpr (!is_iie()) { + if(is_repeat && !repeat_is_pressed_) return true; + } + // If no ASCII value is supplied, look for a few special cases. switch(key) { case Key::Left: value = 0x08; break; @@ -323,17 +333,27 @@ template class ConcreteMachine: } case Key::LeftControl: - control_is_pressed = is_pressed; + control_is_pressed_ = is_pressed; return true; case Key::LeftShift: case Key::RightShift: - shift_is_pressed = is_pressed; + shift_is_pressed_ = is_pressed; return true; case Key::F1: case Key::F2: case Key::F3: case Key::F4: case Key::F5: case Key::F6: case Key::F7: case Key::F8: - case Key::F9: case Key::F10: case Key::F11: case Key::F12: + case Key::F9: case Key::F10: case Key::F11: + repeat_is_pressed_ = is_pressed; + + if constexpr (!is_iie()) { + if(is_pressed && (!is_repeat || character_is_pressed_)) { + keyboard_input_ = uint8_t(last_pressed_character_ | 0x80); + } + } + return true; + + case Key::F12: case Key::PrintScreen: case Key::ScrollLock: case Key::Pause: @@ -356,10 +376,10 @@ template class ConcreteMachine: // Prior to the IIe, the keyboard could produce uppercase only. if(!is_iie()) value = char(toupper(value)); - if(control_is_pressed && isalpha(value)) value &= 0xbf; + if(control_is_pressed_ && isalpha(value)) value &= 0xbf; // TODO: properly map IIe keys - if(!is_iie() && shift_is_pressed) { + if(!is_iie() && shift_is_pressed_) { switch(value) { case 0x27: value = 0x22; break; // ' -> " case 0x2c: value = 0x3c; break; // , -> < @@ -383,11 +403,16 @@ template class ConcreteMachine: } if(is_pressed) { - keyboard_input = uint8_t(value | 0x80); - key_is_down = true; + last_pressed_character_ = value; + character_is_pressed_ = true; + keyboard_input_ = uint8_t(value | 0x80); + key_is_down_ = true; } else { - if((keyboard_input & 0x3f) == value) { - key_is_down = false; + if(value == last_pressed_character_) { + character_is_pressed_ = false; + } + if((keyboard_input_ & 0x3f) == value) { + key_is_down_ = false; } } @@ -395,24 +420,54 @@ template class ConcreteMachine: } uint8_t get_keyboard_input() { - if(string_serialiser) { - return string_serialiser->head() | 0x80; + if(string_serialiser_) { + return string_serialiser_->head() | 0x80; } else { - return keyboard_input; + return keyboard_input_; } } - bool shift_is_pressed = false; - bool control_is_pressed = false; + void clear_keyboard_input() { + keyboard_input_ &= 0x7f; + if(string_serialiser_ && !string_serialiser_->advance()) { + string_serialiser_.reset(); + } + } + + bool get_key_is_down() { + return key_is_down_; + } + + void set_string_serialiser(std::unique_ptr &&serialiser) { + string_serialiser_ = std::move(serialiser); + } + // The IIe has three keys that are wired directly to the same input as the joystick buttons. bool open_apple_is_pressed = false; bool closed_apple_is_pressed = false; - uint8_t keyboard_input = 0x00; - bool key_is_down = false; - std::unique_ptr string_serialiser; private: - Processor *const m6502_; + // Current keyboard input register, as exposed to the programmer; on the IIe the programmer + // can also poll for whether any key is currently down. + uint8_t keyboard_input_ = 0x00; + bool key_is_down_ = false; + + // ASCII input state, referenced by the REPT key on models before the IIe. + char last_pressed_character_ = 0; + bool character_is_pressed_ = false; + + // The repeat key itself. + bool repeat_is_pressed_ = false; + + // Modifier states. + bool shift_is_pressed_ = false; + bool control_is_pressed_ = false; + + // A string serialiser for receiving copy and paste. + std::unique_ptr string_serialiser_; + + // 6502 connection, for applying the reset button. + Processor *const m6502_; }; Keyboard keyboard_; @@ -727,15 +782,11 @@ template class ConcreteMachine: break; case 0xc010: - keyboard_.keyboard_input &= 0x7f; - if(keyboard_.string_serialiser) { - if(!keyboard_.string_serialiser->advance()) - keyboard_.string_serialiser.reset(); - } + keyboard_.clear_keyboard_input(); // On the IIe, reading C010 returns additional key info. if(is_iie() && isReadOperation(operation)) { - *value = (keyboard_.key_is_down ? 0x80 : 0x00) | (keyboard_.keyboard_input & 0x7f); + *value = (keyboard_.get_key_is_down() ? 0x80 : 0x00) | (keyboard_.get_keyboard_input() & 0x7f); } break; @@ -879,7 +930,7 @@ template class ConcreteMachine: } void type_string(const std::string &string) final { - keyboard_.string_serialiser = std::make_unique(string, true); + keyboard_.set_string_serialiser(std::make_unique(string, true)); } bool can_type(char c) const final { @@ -934,15 +985,15 @@ template class ConcreteMachine: using namespace Apple::II; -Machine *Machine::AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { +std::unique_ptr Machine::AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { using Target = Analyser::Static::AppleII::Target; const Target *const appleii_target = dynamic_cast(target); switch(appleii_target->model) { default: return nullptr; - case Target::Model::II: return new ConcreteMachine(*appleii_target, rom_fetcher); - case Target::Model::IIplus: return new ConcreteMachine(*appleii_target, rom_fetcher); - case Target::Model::IIe: return new ConcreteMachine(*appleii_target, rom_fetcher); - case Target::Model::EnhancedIIe: return new ConcreteMachine(*appleii_target, rom_fetcher); + case Target::Model::II: return std::make_unique>(*appleii_target, rom_fetcher); + case Target::Model::IIplus: return std::make_unique>(*appleii_target, rom_fetcher); + case Target::Model::IIe: return std::make_unique>(*appleii_target, rom_fetcher); + case Target::Model::EnhancedIIe: return std::make_unique>(*appleii_target, rom_fetcher); } } diff --git a/Machines/Apple/AppleII/AppleII.hpp b/Machines/Apple/AppleII/AppleII.hpp index 97d348578..047fa7552 100644 --- a/Machines/Apple/AppleII/AppleII.hpp +++ b/Machines/Apple/AppleII/AppleII.hpp @@ -6,8 +6,7 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef AppleII_hpp -#define AppleII_hpp +#pragma once #include "../../../Configurable/Configurable.hpp" #include "../../../Configurable/StandardOptions.hpp" @@ -16,15 +15,14 @@ #include -namespace Apple { -namespace II { +namespace Apple::II { class Machine { public: virtual ~Machine(); /// Creates and returns an AppleII. - static Machine *AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); + static std::unique_ptr AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); /// Defines the runtime options available for an Apple II. class Options: public Reflection::StructImpl, public Configurable::DisplayOption { @@ -43,6 +41,3 @@ class Machine { }; } -} - -#endif /* AppleII_hpp */ diff --git a/Machines/Apple/AppleII/AuxiliaryMemorySwitches.hpp b/Machines/Apple/AppleII/AuxiliaryMemorySwitches.hpp index 389083b95..9c2e760ff 100644 --- a/Machines/Apple/AppleII/AuxiliaryMemorySwitches.hpp +++ b/Machines/Apple/AppleII/AuxiliaryMemorySwitches.hpp @@ -6,13 +6,11 @@ // Copyright © 2020 Thomas Harte. All rights reserved. // -#ifndef AuxiliaryMemorySwitches_h -#define AuxiliaryMemorySwitches_h +#pragma once #include "MemorySwitches.hpp" -namespace Apple { -namespace II { +namespace Apple::II { /*! Models the auxiliary memory soft switches, added as of the Apple IIe, which allow access to the auxiliary 64kb of RAM and to @@ -272,6 +270,3 @@ template class AuxiliaryMemorySwitches { }; } -} - -#endif /* AuxiliaryMemorySwitches_h */ diff --git a/Machines/Apple/AppleII/Card.hpp b/Machines/Apple/AppleII/Card.hpp index 60c3bd519..a5b6f0afa 100644 --- a/Machines/Apple/AppleII/Card.hpp +++ b/Machines/Apple/AppleII/Card.hpp @@ -6,15 +6,13 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef Card_h -#define Card_h +#pragma once #include "../../../Processors/6502/6502.hpp" #include "../../../ClockReceiver/ClockReceiver.hpp" #include "../../../Activity/Observer.hpp" -namespace Apple { -namespace II { +namespace Apple::II { /*! This provides a small subset of the interface offered to cards installed in @@ -113,6 +111,3 @@ class Card { }; } -} - -#endif /* Card_h */ diff --git a/Machines/Apple/AppleII/DiskIICard.hpp b/Machines/Apple/AppleII/DiskIICard.hpp index 979d31b07..4048dc6b4 100644 --- a/Machines/Apple/AppleII/DiskIICard.hpp +++ b/Machines/Apple/AppleII/DiskIICard.hpp @@ -6,8 +6,7 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef DiskIICard_hpp -#define DiskIICard_hpp +#pragma once #include "Card.hpp" #include "../../ROMMachine.hpp" @@ -20,8 +19,7 @@ #include #include -namespace Apple { -namespace II { +namespace Apple::II { class DiskIICard: public Card, public ClockingHint::Observer { public: @@ -44,6 +42,3 @@ class DiskIICard: public Card, public ClockingHint::Observer { }; } -} - -#endif /* DiskIICard_hpp */ diff --git a/Machines/Apple/AppleII/Joystick.hpp b/Machines/Apple/AppleII/Joystick.hpp index 33f943c54..0f623377c 100644 --- a/Machines/Apple/AppleII/Joystick.hpp +++ b/Machines/Apple/AppleII/Joystick.hpp @@ -6,16 +6,14 @@ // Copyright © 2021 Thomas Harte. All rights reserved. // -#ifndef AppleII_Joystick_hpp -#define AppleII_Joystick_hpp +#pragma once #include "../../../Inputs/Joystick.hpp" #include #include -namespace Apple { -namespace II { +namespace Apple::II { class JoystickPair { public: @@ -107,6 +105,3 @@ class JoystickPair { }; } -} - -#endif /* AppleII_Joystick_hpp */ diff --git a/Machines/Apple/AppleII/LanguageCardSwitches.hpp b/Machines/Apple/AppleII/LanguageCardSwitches.hpp index 873260a36..12441d50f 100644 --- a/Machines/Apple/AppleII/LanguageCardSwitches.hpp +++ b/Machines/Apple/AppleII/LanguageCardSwitches.hpp @@ -6,13 +6,11 @@ // Copyright © 2020 Thomas Harte. All rights reserved. // -#ifndef LanguageCardSwitches_h -#define LanguageCardSwitches_h +#pragma once #include "MemorySwitches.hpp" -namespace Apple { -namespace II { +namespace Apple::II { /*! Models the language card soft switches, present on any Apple II with a language card and provided built-in from the IIe onwards. @@ -113,6 +111,3 @@ template class LanguageCardSwitches { }; } -} - -#endif /* LanguageCard_h */ diff --git a/Machines/Apple/AppleII/MemorySwitches.hpp b/Machines/Apple/AppleII/MemorySwitches.hpp index c71bd4c1e..4b192ddf0 100644 --- a/Machines/Apple/AppleII/MemorySwitches.hpp +++ b/Machines/Apple/AppleII/MemorySwitches.hpp @@ -6,11 +6,9 @@ // Copyright © 2022 Thomas Harte. All rights reserved. // -#ifndef MemorySwitches_h -#define MemorySwitches_h +#pragma once -namespace Apple { -namespace II { +namespace Apple::II { enum PagingType: int { Main = 1 << 0, @@ -20,6 +18,3 @@ enum PagingType: int { }; } -} - -#endif /* MemorySwitches_h */ diff --git a/Machines/Apple/AppleII/SCSICard.cpp b/Machines/Apple/AppleII/SCSICard.cpp index 5d18adf3b..9e0e83dd0 100644 --- a/Machines/Apple/AppleII/SCSICard.cpp +++ b/Machines/Apple/AppleII/SCSICard.cpp @@ -167,3 +167,7 @@ void SCSICard::perform_bus_operation(Select select, bool is_read, uint16_t addre void SCSICard::set_storage_device(const std::shared_ptr &device) { storage_->set_storage(device); } + +void SCSICard::set_activity_observer(Activity::Observer *observer) { + scsi_bus_.set_activity_observer(observer); +} diff --git a/Machines/Apple/AppleII/SCSICard.hpp b/Machines/Apple/AppleII/SCSICard.hpp index 8ce7ce20d..40ce1a1b7 100644 --- a/Machines/Apple/AppleII/SCSICard.hpp +++ b/Machines/Apple/AppleII/SCSICard.hpp @@ -6,8 +6,7 @@ // Copyright © 2022 Thomas Harte. All rights reserved. // -#ifndef SCSICard_hpp -#define SCSICard_hpp +#pragma once #include "Card.hpp" #include "../../ROMMachine.hpp" @@ -21,8 +20,7 @@ #include #include -namespace Apple { -namespace II { +namespace Apple::II { class SCSICard: public Card { public: @@ -37,6 +35,8 @@ class SCSICard: public Card { scsi_bus_.run_for(cycles); } + void set_activity_observer(Activity::Observer *observer) final; + private: uint8_t *ram_pointer_ = nullptr; uint8_t *rom_pointer_ = nullptr; @@ -50,6 +50,3 @@ class SCSICard: public Card { }; } -} - -#endif /* SCSICard_hpp */ diff --git a/Machines/Apple/AppleII/Video.hpp b/Machines/Apple/AppleII/Video.hpp index 1e66d1cc2..26e230b1d 100644 --- a/Machines/Apple/AppleII/Video.hpp +++ b/Machines/Apple/AppleII/Video.hpp @@ -6,8 +6,7 @@ // Copyright 2018 Thomas Harte. All rights reserved. // -#ifndef Apple_II_Video_hpp -#define Apple_II_Video_hpp +#pragma once #include "../../../Outputs/CRT/CRT.hpp" #include "../../../ClockReceiver/ClockReceiver.hpp" @@ -18,9 +17,7 @@ #include #include -namespace Apple { -namespace II { -namespace Video { +namespace Apple::II::Video { class BusHandler { public: @@ -131,14 +128,56 @@ template class Video: public VideoBase { */ uint8_t get_last_read_value(Cycles offset) { // Rules of generation: - // (1) a complete sixty-five-cycle scan line consists of sixty-five consecutive bytes of - // display buffer memory that starts twenty-five bytes prior to the actual data to be displayed. - // (2) During VBL the data acts just as if it were starting a whole new frame from the beginning, but - // it never finishes this pseudo-frame. After getting one third of the way through the frame (to - // scan line $3F), it suddenly repeats the previous six scan lines ($3A through $3F) before aborting - // to begin the next true frame. + + // FOR ALL MODELS IN ALL MODES: // - // Source: Have an Apple Split by Bob Bishop; http://rich12345.tripod.com/aiivideo/softalk.html + // - "Screen memory is divided into 128-byte segments. Each segment is divided into the FIRST 40, the + // SECOND 40, the THIRD 40, and eight bytes of no man's memory (UNUSED 8)." (5-8*) + // + // - "The VBL base addresses are equal to the FIRST 40 base addresses minus eight bytes using 128-byte + // wraparound subtraction. Example: $400 minus $8 gives $478; not $3F8." (5-11*) + // + // - "The memory locations scanned during HBL prior to a displayed line are the 24 bytes just below the + // displayed area, using 128-byte wraparound addressing." (5-13*) + // + // - "The first address of HBL is always addressed twice consecutively" (5-11*) + // + // - "Memory scanned by lines 256 through 261 is identical to memory scanned by lines 250 through 255, + // so those six 64-byte sections are scanned twice" (5-13*) + + // FOR II AND II+ ONLY (NOT IIE OR LATER) IN TEXT/LORES MODE ONLY (NOT HIRES): + // + // - "HBL scanned memory begins $18 bytes before display scanned memory plus $1000." (5-11*) + // + // - "Horizontal scanning wraps around at the 128-byte segment boundaries. Example: the address scanned + // before address $400 is $47F during VBL. The address scanned before $400 when VBL is false is + // $147F." (5-11*) + // + // - "the memory scanned during HBL is completely separate from the memory scanned during HBL´." (5-11*) + // + // - "HBL scanned memory is in an area normally taken up by Applesoft programs or Integer BASIC + // variables" (5-37*) + // + // - Figure 5.17 Screen Memory Scanning (5-37*) + + // FOR IIE AND LATER IN ALL MODES AND II AND II+ IN HIRES MODE: + // + // - "HBL scanned memory begins $18 bytes before display scanned memory." (5-10**) + // + // - "Horizontal scanning wraps around at the 128-byte segment boundaries. Example: the address scanned + // before address $400 is $47F." (5-11**) + // + // - "during HBL, the memory locations that are scanned are in the displayed memory area." (5-13*) + // + // - "Programs written for the Apple II may well not perform correctly on the Apple IIe because of + // differences in scanning during HBL. In the Apple II, HBL scanned memory was separate from other + // display memory in TEXT/LORES scanning. In the Apple IIe, HBL scanned memory overlaps other scanned + // memory in TEXT/LORES scanning in similar fashion to HIRES scanning." (5-43**) + // + // - Figure 5.17 Display Memory Scanning (5-41**) + + // Source: * Understanding the Apple II by Jim Sather + // Source: ** Understanding the Apple IIe by Jim Sather // Determine column at offset. int mapped_column = column_ + int(offset.as_integral()); @@ -149,8 +188,21 @@ template class Video: public VideoBase { // Apply carry into the row counter. int mapped_row = row_ + (mapped_column / 65); - mapped_column %= 65; mapped_row %= 262; + mapped_column %= 65; + + // Remember if we're in a horizontal blanking interval. + int hbl = mapped_column < 25; + + // The first column is read twice. + if(mapped_column == 0) { + mapped_column = 1; + } + + // Vertical blanking rows read eight bytes earlier. + if(mapped_row >= 192) { + mapped_column -= 8; + } // Apple out-of-bounds row logic. if(mapped_row >= 256) { @@ -159,8 +211,24 @@ template class Video: public VideoBase { mapped_row %= 192; } - // Calculate the address and return the value. + // Calculate the address. uint16_t read_address = uint16_t(get_row_address(mapped_row) + mapped_column - 25); + + // Wraparound addressing within 128 byte sections. + if(mapped_row < 64 && mapped_column < 25) { + read_address += 128; + } + + if(hbl && !is_iie_) { + // On Apple II and II+ (not IIe or later) in text/lores mode (not hires), horizontal + // blanking bytes read from $1000 higher. + const GraphicsMode pixel_mode = graphics_mode(mapped_row); + if((pixel_mode == GraphicsMode::Text) || (pixel_mode == GraphicsMode::LowRes)) { + read_address += 0x1000; + } + } + + // Read the address and return the value. uint8_t value, aux_value; bus_handler_.perform_read(read_address, 1, &value, &aux_value); return value; @@ -170,20 +238,20 @@ template class Video: public VideoBase { @returns @c true if the display will be within vertical blank at now + @c offset; @c false otherwise. */ bool get_is_vertical_blank(Cycles offset) { - // Map that backwards from the internal pixels-at-start generation to pixels-at-end - // (so what was column 0 is now column 25). + // Determine column at offset. int mapped_column = column_ + int(offset.as_integral()); // Map that backwards from the internal pixels-at-start generation to pixels-at-end // (so what was column 0 is now column 25). mapped_column += 25; - // Apply carry into the row counter and test it for location. + // Apply carry into the row counter. int mapped_row = row_ + (mapped_column / 65); + mapped_row %= 262; // Per http://www.1000bit.it/support/manuali/apple/technotes/iigs/tn.iigs.040.html // "on the IIe, the screen is blanked when the bit is low". - return (mapped_row % 262) < 192; + return mapped_row < 192; } private: @@ -211,7 +279,7 @@ template class Video: public VideoBase { if(is_vertical_sync_line) { // In effect apply an XOR to HSYNC and VSYNC flags in order to include equalising - // pulses (and hencce keep hsync approximately where it should be during vsync). + // pulses (and hence keep hsync approximately where it should be during vsync). const int blank_start = std::max(first_sync_column - sync_length, column_); const int blank_end = std::min(first_sync_column, ending_column); if(blank_end > blank_start) { @@ -391,7 +459,10 @@ template class Video: public VideoBase { } int second_blank_start; - if(!is_text_mode(graphics_mode(row_+1))) { + // Colour burst is present on all lines of the display if graphics mode is enabled on the top + // portion; therefore use the graphics mode on line 0 rather than the current line, to avoid + // disabling it in mixed modes. + if(!is_text_mode(graphics_mode(0))) { const int colour_burst_start = std::max(first_sync_column + sync_length + 1, column_); const int colour_burst_end = std::min(first_sync_column + sync_length + 4, ending_column); if(colour_burst_end > colour_burst_start) { @@ -400,9 +471,9 @@ template class Video: public VideoBase { // Supply the real phase value if this is an Apple build. // TODO: eliminate UGLY HACK. #if defined(__APPLE__) && !defined(IGNORE_APPLE) - constexpr int phase = 224; + constexpr uint8_t phase = 224; #else - constexpr int phase = 0; + constexpr uint8_t phase = 192; #endif crt_.output_colour_burst((colour_burst_end - colour_burst_start) * 14, phase); @@ -440,7 +511,3 @@ template class Video: public VideoBase { }; } -} -} - -#endif /* Apple_II_Video_hpp */ diff --git a/Machines/Apple/AppleII/VideoSwitches.hpp b/Machines/Apple/AppleII/VideoSwitches.hpp index 7852516f4..159cd0491 100644 --- a/Machines/Apple/AppleII/VideoSwitches.hpp +++ b/Machines/Apple/AppleII/VideoSwitches.hpp @@ -6,15 +6,13 @@ // Copyright © 2020 Thomas Harte. All rights reserved. // -#ifndef VideoSwitches_h -#define VideoSwitches_h +#pragma once #include "../../../ClockReceiver/ClockReceiver.hpp" #include "../../../ClockReceiver/DeferredQueue.hpp" #include "../../ROMMachine.hpp" -namespace Apple { -namespace II { +namespace Apple::II { // Enumerates all Apple II and IIe display modes. enum class GraphicsMode { @@ -40,7 +38,7 @@ template class VideoSwitches { set of potential flashing characters and alternate video modes. */ VideoSwitches(bool is_iie, TimeUnit delay, std::function &&target) : delay_(delay), deferrer_(std::move(target)) { - character_zones_[0].xor_mask = 0; + character_zones_[0].xor_mask = 0xff; character_zones_[0].address_mask = 0x3f; character_zones_[1].xor_mask = 0; character_zones_[1].address_mask = 0x3f; @@ -50,7 +48,7 @@ template class VideoSwitches { character_zones_[3].address_mask = 0x3f; if(is_iie) { - character_zones_[0].xor_mask = + character_zones_[1].xor_mask = character_zones_[2].xor_mask = character_zones_[3].xor_mask = 0xff; character_zones_[2].address_mask = @@ -89,7 +87,7 @@ template class VideoSwitches { if(alternative_character_set) { character_zones_[1].address_mask = 0xff; - character_zones_[1].xor_mask = 0; + character_zones_[1].xor_mask = 0xff; } else { character_zones_[1].address_mask = 0x3f; character_zones_[1].xor_mask = flash_mask(); @@ -287,7 +285,9 @@ template class VideoSwitches { // Update character set flashing; flashing is applied only when the alternative // character set is not selected. flash_ = (flash_ + 1) % (2 * flash_length); - character_zones_[1].xor_mask = flash_mask() * !internal_.alternative_character_set; + if(!internal_.alternative_character_set) { + character_zones_[1].xor_mask = flash_mask(); + } } private: @@ -342,6 +342,3 @@ template class VideoSwitches { }; } -} - -#endif /* VideoSwitches_h */ diff --git a/Machines/Apple/AppleIIgs/ADB.cpp b/Machines/Apple/AppleIIgs/ADB.cpp index 7276a2801..283109af0 100644 --- a/Machines/Apple/AppleIIgs/ADB.cpp +++ b/Machines/Apple/AppleIIgs/ADB.cpp @@ -16,7 +16,6 @@ #include "../../../InstructionSets/M50740/Parser.hpp" #include "../../../InstructionSets/Disassembler.hpp" -#define LOG_PREFIX "[ADB GLU] " #include "../../../Outputs/Log.hpp" using namespace Apple::IIgs::ADB; @@ -40,6 +39,8 @@ enum class MicrocontrollerFlags: uint8_t { CommandRegisterFull = 0x40, }; +Log::Logger logger; + } GLU::GLU() : @@ -71,16 +72,20 @@ uint8_t GLU::get_any_key_down() { uint8_t GLU::get_mouse_data() { // Alternates between returning x and y values. // - // b7: 1 = button is up; 0 = button is down. + // b7: 1 = button is up; 0 = button is down. // b6: delta sign bit; 1 = negative. // b5–b0: mouse delta. const uint8_t result = registers_[visible_mouse_register_]; - if(visible_mouse_register_ == 2) { - ++visible_mouse_register_; - } else { + if(visible_mouse_register_ == 3) { status_ &= ~uint8_t(CPUFlags::MouseDataFull); } + + // Spelt out at tedious length because Clang has trust issues. + static constexpr int first_register = 2; + static constexpr int second_register = 3; + static constexpr int flip_mask = first_register ^ second_register; + visible_mouse_register_ ^= flip_mask; return result; } @@ -116,7 +121,7 @@ uint8_t GLU::get_status() { // b2: 1 = keyboard data interrupt is enabled. // b1: 1 = mouse x-data is available; 0 = y. // b0: 1 = command register is full (set when command is written); 0 = empty (cleared when data is read). - return status_ | ((visible_mouse_register_ == 2) ? uint8_t(CPUFlags::MouseXIsAvailable) : 0); + return status_ | ((visible_mouse_register_ == 2) ? 0 : uint8_t(CPUFlags::MouseXIsAvailable)); } void GLU::set_status(uint8_t status) { @@ -223,7 +228,6 @@ void GLU::set_port_output(int port, uint8_t value) { case 3: status_ |= uint8_t(CPUFlags::MouseDataFull); visible_mouse_register_ = 2; - printf("Mouse: %d <- %02x\n", register_address_, register_latch_); break; case 7: status_ |= uint8_t(CPUFlags::CommandDataIsValid); break; } @@ -243,7 +247,7 @@ void GLU::set_port_output(int port, uint8_t value) { case 3: if(modifier_state_ != (value & 0x30)) { modifier_state_ = value & 0x30; - LOG("Modifier state: " << int(value & 0x30)); + logger.info().append("Modifier state: %02x", modifier_state_); } // Output is inverted respective to input; the microcontroller @@ -286,5 +290,6 @@ void GLU::run_ports_for(Cycles cycles) { } void GLU::set_vertical_blank(bool is_blank) { + vertical_blank_ = is_blank; executor_.set_interrupt_line(is_blank); } diff --git a/Machines/Apple/AppleIIgs/ADB.hpp b/Machines/Apple/AppleIIgs/ADB.hpp index a57594335..9669c0f19 100644 --- a/Machines/Apple/AppleIIgs/ADB.hpp +++ b/Machines/Apple/AppleIIgs/ADB.hpp @@ -6,8 +6,7 @@ // Copyright © 2020 Thomas Harte. All rights reserved. // -#ifndef Apple_IIgs_ADB_hpp -#define Apple_IIgs_ADB_hpp +#pragma once #include #include @@ -17,9 +16,7 @@ #include "../ADB/Mouse.hpp" #include "../ADB/Keyboard.hpp" -namespace Apple { -namespace IIgs { -namespace ADB { +namespace Apple::IIgs::ADB { class GLU: public InstructionSet::M50740::PortHandler { public: @@ -86,7 +83,3 @@ class GLU: public InstructionSet::M50740::PortHandler { }; } -} -} - -#endif /* Apple_IIgs_ADB_hpp */ diff --git a/Machines/Apple/AppleIIgs/AppleIIgs.cpp b/Machines/Apple/AppleIIgs/AppleIIgs.cpp index 5bc2a1afe..06f48ad44 100644 --- a/Machines/Apple/AppleIIgs/AppleIIgs.cpp +++ b/Machines/Apple/AppleIIgs/AppleIIgs.cpp @@ -71,115 +71,6 @@ constexpr uint8_t default_bram[] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfd, 0x96, 0x57, 0x3c, }; -/*class MemManagerChecker { - int handle_total_ = 0; - bool dump_bank(const Apple::IIgs::MemoryMap &memory, const char *name, uint32_t address, bool print, uint32_t must_contain = 0xffffffff) { - const auto handles = memory.regions[memory.region_map[0xe117]].read; - bool did_find = false; - - if(print) printf("%s: ", name); - int max = 52; - uint32_t last_visited = 0; - - // Seed address. - address = uint32_t(handles[address] | (handles[address+1] << 8) | (handles[address+2] << 16) | (handles[address+3] << 24)); - - while(true) { - did_find |= address == must_contain; - - if(!address) { - if(print) printf("nil\n"); - break; - } - ++handle_total_; - if(address < 0xe11700 || address > 0xe11aff) { - if(print) printf("Out of bounds error with address = %06x!\n", address); - return false; - } - if((address - 0xe11700)%20) { - if(print) printf("Address alignment error!\n"); - return false; - } - - const uint32_t previous = uint32_t(handles[address+12] | (handles[address+13] << 8) | (handles[address+14] << 16) | (handles[address+15] << 24)); - const uint32_t next = uint32_t(handles[address+16] | (handles[address+17] << 8) | (handles[address+18] << 16) | (handles[address+19] << 24)); - const uint32_t pointer = uint32_t(handles[address] | (handles[address+1] << 8) | (handles[address+2] << 16) | (handles[address+3] << 24)); - const uint32_t size = uint32_t(handles[address+8] | (handles[address+9] << 8) | (handles[address+10] << 16) | (handles[address+11] << 24)); - if(print) printf("%06x (<- %06x | %06x ->) [%06x:%06x] -> \n", address, previous, next, pointer, size); - - if(previous && ((previous < 0xe0'0000) || (previous > 0xe2'0000))) { - if(print) printf("Out of bounds error with previous = %06x! [%d && (%d || %d)]\n", previous, bool(previous), previous < 0xe0'0000, previous > 0xe2'0000); - return false; - } - if((previous || last_visited) && (previous != last_visited)) { - if(print) printf("Back link error!\n"); - return false; - } - - last_visited = address; - address = next; - - --max; - if(!max) { - if(print) printf("Endless loop error!\n"); - return false; - } - } - - if(must_contain != 0xffffffff) { - if(!did_find) { - if(print) printf("%08x not found\n", must_contain); - return false; - } - } - - return true; - } - -// bool has_seen_valid_memory_ = false; -// bool should_validate_ = false; - - public: - bool validate_memory_manager(const Apple::IIgs::MemoryMap &memory, bool print) { - const auto pointers = memory.regions[memory.region_map[0xe116]].read; - - constexpr uint32_t address = 0xe1162c; - const uint32_t last_high_handle = uint32_t(pointers[address] | (pointers[address+1] << 8) | (pointers[address+2] << 16) | (pointers[address+3] << 24)); - - // Check for initial state having been reached. -// if(!has_seen_valid_memory_) { -// if(pointers[0xe11624]) return true; -// for(int c = 0xe1160c; c < 0xe1161c; c++) { -// if(pointers[c]) return true; -// } -// has_seen_valid_memory_ = true; -// } - - // Output. - if(print) printf("\nNumber of banks: %d\n", pointers[0xe11624]); - if(print) printf("Last high handle: %04x\n", last_high_handle); - bool result = true; - - handle_total_ = 0; - result &= dump_bank(memory, "Mem", 0xe11600, print, last_high_handle); - result &= dump_bank(memory, "Purge", 0xe11604, print); - result &= dump_bank(memory, "Free", 0xe11608, print); - - // TODO: and other checs? - -// result &= dump_bank("Bank 0", 0xe1160c); -// result &= dump_bank("Bank 1", 0xe11610); -// result &= dump_bank("Bank E0", 0xe11614); -// result &= dump_bank("Bank E1", 0xe11618); -// result &= dump_bank("Bank FF", 0xe1161c); - - if(print) printf("Total: %d\n", handle_total_); - if(handle_total_ != 51) result &= false; - - return result; - } -};*/ - } namespace Apple { @@ -203,8 +94,8 @@ class ConcreteMachine: memory_(target.model >= Analyser::Static::AppleIIgs::Target::Model::ROM03), iwm_(CLOCK_RATE / 2), drives35_{ - {CLOCK_RATE / 2, true}, - {CLOCK_RATE / 2, true} + {CLOCK_RATE / 2, true}, + {CLOCK_RATE / 2, true} }, drives525_{ {CLOCK_RATE / 2}, @@ -305,7 +196,6 @@ class ConcreteMachine: iwm_->set_drive(1, &drives35_[1]); // Randomise RAM contents. -// std::srand(23); Memory::Fuzz(ram_); // Prior to ROM03 there's no power-on bit. @@ -382,29 +272,10 @@ class ConcreteMachine: } // MARK: BusHandler. - uint64_t total = 0; forceinline Cycles perform_bus_operation(const CPU::WDC65816::BusOperation operation, const uint32_t address, uint8_t *const value) { - const auto ®ion = MemoryMapRegion(memory_, address); - static bool log = false; + const auto ®ion = memory_.region(address); bool is_1Mhz = false; -// if(operation == CPU::WDC65816::BusOperation::ReadOpcode) { -// if(address == 0xfe00d5) { -// printf(""); -// } -// -// printf("%06x a:%04x x:%04x y:%04x s:%04x d:%04x b:%04x\n", -// address, -// m65816_.get_value_of_register(CPU::WDC65816::Register::A), -// m65816_.get_value_of_register(CPU::WDC65816::Register::X), -// m65816_.get_value_of_register(CPU::WDC65816::Register::Y), -//// m65816_.get_value_of_register(CPU::WDC65816::Register::Flags), -// m65816_.get_value_of_register(CPU::WDC65816::Register::StackPointer), -// m65816_.get_value_of_register(CPU::WDC65816::Register::Direct), -// m65816_.get_value_of_register(CPU::WDC65816::Register::DataBank) -// ); -// } - if(operation == CPU::WDC65816::BusOperation::ReadVector && !(memory_.get_shadow_register()&0x40)) { // I think vector pulls always go to ROM? // That's slightly implied in the documentation, and doing so makes GS/OS boot, so... @@ -514,6 +385,11 @@ class ConcreteMachine: video_->set_page2(*value & 0x40); break; + case Read(0xc069): + case Write(0xc069): + // Swallow silently; often hit as a side effect of a 16-bit write to 0xc068. + break; + // Various independent memory switch reads [TODO: does the IIe-style keyboard provide the low seven?]. #define SwitchRead(s) *value = memory_.s ? 0x80 : 0x00; is_1Mhz = true; #define LanguageRead(s) SwitchRead(language_card_switches().state().s) @@ -752,7 +628,7 @@ class ConcreteMachine: is_1Mhz = true; break; case Read(0xc045): - // MMDELTAX byte. + // MMDELTAY byte. *value = 0; is_1Mhz = true; break; @@ -899,7 +775,6 @@ class ConcreteMachine: // Temporary: log _potential_ mistakes. if((address_suffix < 0xc100 && address_suffix >= 0xc090) || (address_suffix < 0xc080)) { printf("Internal card-area access: %04x\n", address_suffix); -// log |= operation == CPU::WDC65816::BusOperation::ReadOpcode; } if(is_read) { *value = rom_[rom_.size() - 65536 + address_suffix]; @@ -934,7 +809,6 @@ class ConcreteMachine: if(address_suffix < 0xc080) { // TODO: all other IO accesses. printf("Unhandled IO %s: %04x\n", is_read ? "read" : "write", address_suffix); -// assert(false); } } } @@ -945,7 +819,7 @@ class ConcreteMachine: is_1Mhz = region.flags & MemoryMap::Region::Is1Mhz; if(isReadOperation(operation)) { - MemoryMapRead(region, address, value); + *value = memory_.read(region, address); } else { // Shadowed writes also occur "at 1Mhz". // TODO: this is probably an approximation. I'm assuming that there's the ability asynchronously to post @@ -954,7 +828,7 @@ class ConcreteMachine: // get by adding periodic NOPs within their copy-to-shadow step. // // Maybe the interaction with 2.8Mhz refresh isn't as straightforward as I think? - const bool is_shadowed = IsShadowed(memory_, region, address); + const bool is_shadowed = memory_.is_shadowed(region, address); is_1Mhz |= is_shadowed; // Use a very broad test for flushing video: any write to $e0 or $e1, or any write that is shadowed. @@ -963,67 +837,10 @@ class ConcreteMachine: video_.flush(); } - MemoryMapWrite(memory_, region, address, value); + memory_.write(region, address, *value); } } - - if(operation == CPU::WDC65816::BusOperation::ReadOpcode) { -// if(total >= 92168628 && !validate_memory_manager(false) && address < 0xe10000) { -// printf("@%llu\n", static_cast(total)); -// validate_memory_manager(true); -// } -// assert(address); - } - -// if(total == 132222166 || total == 467891275 || total == 491026055) { -// validate_memory_manager(true); -// } - -// if(operation == CPU::WDC65816::BusOperation::Write && ( -// (address >= 0xe11700 && address <= 0xe11aff) || -// address == 0xe11624 || (address >= 0xe1160c && address < 0xe1161c)) -// ) { -// // Test for breakages in the chain. -// if(!dump_memory_manager()) { -// printf("Broken at %llu\n", static_cast(total)); -// } else { -// printf("Correct at %llu\n", static_cast(total)); -// } -// } - - if(operation == CPU::WDC65816::BusOperation::ReadOpcode) { -// if(total > 482342960 && total < 482352960 && address == 0xe10000) { -// printf("entry: %llu\n", static_cast(total)); -// } - -// log |= address == 0xfc144f; -// log &= !((address < 0xfc144f) || (address >= 0xfc1490)); - -// if(address == 0xfc02b1) { -// dump_memory_manager(); -// } - } - - if(log) { - printf("%06x %s %02x [%s]", address, isReadOperation(operation) ? "->" : "<-", *value, (is_1Mhz || (speed_register_ & motor_flags_)) ? "1.0" : "2.8"); - if(operation == CPU::WDC65816::BusOperation::ReadOpcode) { - printf(" a:%04x x:%04x y:%04x s:%04x e:%d p:%02x db:%02x pb:%02x d:%04x [tot:%llu]\n", - m65816_.get_value_of_register(CPU::WDC65816::Register::A), - m65816_.get_value_of_register(CPU::WDC65816::Register::X), - m65816_.get_value_of_register(CPU::WDC65816::Register::Y), - m65816_.get_value_of_register(CPU::WDC65816::Register::StackPointer), - m65816_.get_value_of_register(CPU::WDC65816::Register::EmulationFlag), - m65816_.get_value_of_register(CPU::WDC65816::Register::Flags), - m65816_.get_value_of_register(CPU::WDC65816::Register::DataBank), - m65816_.get_value_of_register(CPU::WDC65816::Register::ProgramBank), - m65816_.get_value_of_register(CPU::WDC65816::Register::Direct), - static_cast(total) - ); - } else printf("\n"); - } - - Cycles duration; // In preparation for this test: the top bit of speed_register_ has been inverted, @@ -1054,7 +871,6 @@ class ConcreteMachine: fast_access_phase_ = (fast_access_phase_ + duration.as()) % 50; slow_access_phase_ = (slow_access_phase_ + duration.as()) % 912; - // Propagate time far and wide. cycles_since_clock_tick_ += duration; auto ticks = cycles_since_clock_tick_.divide(Cycles(CLOCK_RATE)).as_integral(); @@ -1066,15 +882,10 @@ class ConcreteMachine: update_interrupts(); } -// if(operation == CPU::WDC65816::BusOperation::ReadOpcode && *value == 0x00) { -// printf("%06x: %02x\n", address, *value); -// } - video_ += duration; iwm_ += duration; cycles_since_audio_update_ += duration; adb_glu_ += duration; - total += decltype(total)(duration.as_integral()); if(cycles_since_audio_update_ >= cycles_until_audio_event_) { AudioUpdater updater(this); @@ -1144,9 +955,9 @@ class ConcreteMachine: Apple::Clock::ParallelClock clock_; JustInTimeActor video_; // i.e. run video at 7Mhz. JustInTimeActor adb_glu_; // i.e. 3,579,545Mhz. - Zilog::SCC::z8530 scc_; - JustInTimeActor iwm_; - Cycles cycles_since_clock_tick_; + Zilog::SCC::z8530 scc_; + JustInTimeActor iwm_; + Cycles cycles_since_clock_tick_; Apple::Macintosh::DoubleDensityDrive drives35_[2]; Apple::Disk::DiskIIDrive drives525_[2]; @@ -1171,7 +982,7 @@ class ConcreteMachine: machine_->update_audio(); } ~AudioUpdater() { - machine_->cycles_until_audio_event_ = machine_->sound_glu_.get_next_sequence_point(); + machine_->cycles_until_audio_event_ = machine_->sound_glu_.next_sequence_point(); } private: ConcreteMachine *machine_; @@ -1199,8 +1010,8 @@ class ConcreteMachine: using namespace Apple::IIgs; -Machine *Machine::AppleIIgs(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { - return new ConcreteMachine(*dynamic_cast(target), rom_fetcher); +std::unique_ptr Machine::AppleIIgs(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { + return std::make_unique(*dynamic_cast(target), rom_fetcher); } Machine::~Machine() {} diff --git a/Machines/Apple/AppleIIgs/AppleIIgs.hpp b/Machines/Apple/AppleIIgs/AppleIIgs.hpp index f92bfd11c..252ed1e1a 100644 --- a/Machines/Apple/AppleIIgs/AppleIIgs.hpp +++ b/Machines/Apple/AppleIIgs/AppleIIgs.hpp @@ -6,8 +6,7 @@ // Copyright 2020 Thomas Harte. All rights reserved. // -#ifndef Machines_Apple_AppleIIgs_hpp -#define Machines_Apple_AppleIIgs_hpp +#pragma once #include "../../../Configurable/Configurable.hpp" #include "../../../Configurable/StandardOptions.hpp" @@ -16,18 +15,14 @@ #include -namespace Apple { -namespace IIgs { +namespace Apple::IIgs { class Machine { public: virtual ~Machine(); /// Creates and returns an AppleIIgs. - static Machine *AppleIIgs(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); + static std::unique_ptr AppleIIgs(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); }; } -} - -#endif /* Machines_Apple_AppleIIgs_hpp */ diff --git a/Machines/Apple/AppleIIgs/MemoryMap.cpp b/Machines/Apple/AppleIIgs/MemoryMap.cpp new file mode 100644 index 000000000..04a33caaa --- /dev/null +++ b/Machines/Apple/AppleIIgs/MemoryMap.cpp @@ -0,0 +1,533 @@ +// +// MemoryMap.cpp +// Clock Signal +// +// Created by Thomas Harte on 04/01/2024. +// Copyright © 2024 Thomas Harte. All rights reserved. +// + +#include "MemoryMap.hpp" + +using namespace Apple::IIgs; +using PagingType = Apple::II::PagingType; + +void MemoryMap::set_storage(std::vector &ram, std::vector &rom) { + // Keep a pointer for later; also note the proper RAM offset. + ram_base_ = ram.data(); + shadow_base_[0] = ram_base_; // i.e. all unshadowed writes go to where they've already gone (to make a no-op). + shadow_base_[1] = &ram[ram.size() - 0x02'0000]; // i.e. all shadowed writes go somewhere in the last + // 128bk of RAM. + + // Establish bank mapping. + uint8_t next_region = 0; + auto region = [&]() -> uint8_t { + assert(next_region != this->regions_.size()); + return next_region++; + }; + auto set_region = [this](uint8_t bank, uint16_t start, uint16_t end, uint8_t region) { + assert((end == 0xffff) || !(end&0xff)); + assert(!(start&0xff)); + + // Fill in memory map. + size_t target = size_t((bank << 8) | (start >> 8)); + for(int c = start; c < end; c += 0x100) { + region_map_[target] = region; + ++target; + } + }; + auto set_regions = [set_region, region](uint8_t bank, std::initializer_list addresses, std::vector allocated_regions = {}) { + uint16_t previous = 0x0000; + auto next_region = allocated_regions.begin(); + for(uint16_t address: addresses) { + set_region(bank, previous, address, next_region != allocated_regions.end() ? *next_region : region()); + previous = address; + assert(next_region != allocated_regions.end() || allocated_regions.empty()); + if(next_region != allocated_regions.end()) ++next_region; + } + assert(next_region == allocated_regions.end()); + }; + + // Current beliefs about the IIgs memory map: + // + // * language card banking applies to banks $00, $01, $e0 and $e1; + // * auxiliary memory switches apply to bank $00 only; + // * shadowing may be enabled only on banks $00 and $01, or on all RAM pages; and + // * whether bit 16 of the address is passed to the Mega II is selectable — this affects both the destination + // of odd-bank shadows, and whether bank $e1 is actually distinct from $e0. + // + // So: + // + // * bank $00 needs to be divided by auxiliary and language card zones; + // * banks $01, $e0 and $e1 need to be divided by language card zones only; and + // * ROM banks and all other fast RAM banks don't need subdivision. + + // Language card zones: + // + // $D000–$E000 4kb window, into either bank 1 or bank 2 + // $E000–end 12kb window, always the same RAM. + + // Auxiliary zones: + // + // $0000–$0200 Zero page (and stack) + // $0200–$0400 [space in between] + // $0400–$0800 Text Page 1 + // $0800–$2000 [space in between] + // $2000–$4000 High-res Page 1 + // $4000–$C000 [space in between] + + // Card zones: + // + // $C100–$C2FF either cards or IIe-style ROM + // $C300–$C3FF IIe-supplied 80-column card replacement ROM + // $C400–$C7FF either cards or IIe-style ROM + // $C800–$CFFF Standard extended card area + + // Reserve region 0 as that for unmapped memory. + region(); + + // Bank $00: all locations potentially affected by the auxiliary switches or the + // language switches. + set_regions(0x00, { + 0x0200, 0x0400, 0x0800, + 0x2000, 0x4000, + 0xc000, 0xc100, 0xc300, 0xc400, 0xc800, + 0xd000, 0xe000, + 0xffff + }); + + // Bank $01: all locations potentially affected by the language switches and card switches. + set_regions(0x01, { + 0xc000, 0xc100, 0xc300, 0xc400, 0xc800, + 0xd000, 0xe000, + 0xffff + }); + + // Banks $02–[end of RAM]: a single region. + const auto fast_region = region(); + const uint8_t fast_ram_bank_limit = uint8_t(ram.size() / 0x01'0000); + for(uint8_t bank = 0x02; bank < fast_ram_bank_limit; bank++) { + set_region(bank, 0x0000, 0xffff, fast_region); + } + + // [Banks $80–$e0: empty]. + + // Banks $e0, $e1: all locations potentially affected by the language switches or marked for IO. + // Alas, separate regions are needed due to the same ROM appearing on both pages. + for(uint8_t c = 0; c < 2; c++) { + set_regions(0xe0 + c, {0xc000, 0xc100, 0xc300, 0xc400, 0xc800, 0xd000, 0xe000, 0xffff}); + } + + // [Banks $e2–[ROM start]: empty]. + + // ROM banks: directly mapped to ROM. + const uint8_t rom_bank_count = uint8_t(rom.size() >> 16); + const uint8_t first_rom_bank = uint8_t(0x100 - rom_bank_count); + const uint8_t rom_region = region(); + for(uint8_t c = 0; c < rom_bank_count; ++c) { + set_region(first_rom_bank + c, 0x0000, 0xffff, rom_region); + } + + // Apply proper storage to those banks. + auto set_storage = [this](uint32_t address, const uint8_t *read, uint8_t *write) { + // Don't allow the reserved null region to be modified. + assert(region_map_[address >> 8]); + + // Either set or apply a quick bit of testing as to the logic at play. + auto ®ion = regions_[region_map_[address >> 8]]; + if(read) read -= address; + if(write) write -= address; + if(!region.read) { + region.read = read; + region.write = write; + } else { + assert(region.read == read); + assert(region.write == write); + } + }; + + // This is highly redundant, but decouples this step from the above. + for(size_t c = 0; c < 0x80'0000; c += 0x100) { + if(c < ram.size() - 0x02'0000) { + set_storage(uint32_t(c), &ram[c], &ram[c]); + } + } + uint8_t *const slow_ram = &ram[ram.size() - 0x02'0000] - 0xe0'0000; + for(size_t c = 0xe0'0000; c < 0xe2'0000; c += 0x100) { + set_storage(uint32_t(c), &slow_ram[c], &slow_ram[c]); + } + for(uint32_t c = 0; c < uint32_t(rom_bank_count); c++) { + set_storage((first_rom_bank + c) << 16, &rom[c << 16], nullptr); + } + + // Set shadowing as working from banks 0 and 1 (forever). + shadow_banks_[0] = true; + + // TODO: set 1Mhz flags. + + // Apply initial language/auxiliary state. + set_paging<~0>(); +} + +void MemoryMap::set_shadow_register(uint8_t value) { + const uint8_t diff = value ^ shadow_register_; + shadow_register_ = value; + + if(diff & 0x40) { // IO/language-card inhibit. + set_paging(); + } + + if(diff & 0x3f) { + set_shadowing(); + } +} + +uint8_t MemoryMap::get_shadow_register() const { + return shadow_register_; +} + +void MemoryMap::set_speed_register(uint8_t value) { + speed_register_ = value; + + // Enable or disable shadowing from banks 0x02–0x80. + for(size_t c = 0x01; c < 0x40; c++) { + shadow_banks_[c] = speed_register_ & 0x10; + } +} + +void MemoryMap::set_state_register(uint8_t value) { + auxiliary_switches_.set_state(value); + language_card_.set_state(value); +} + +uint8_t MemoryMap::get_state_register() const { + return language_card_.get_state() | auxiliary_switches_.get_state(); +} + +void MemoryMap::access(uint16_t address, bool is_read) { + auxiliary_switches_.access(address, is_read); + if((address & 0xfff0) == 0xc080) language_card_.access(address, is_read); +} + +void MemoryMap::assert_is_region(uint8_t start, uint8_t end) { + assert(region_map_[start] == region_map_[start-1]+1); + assert(region_map_[end-1] == region_map_[start]); + assert(region_map_[end] == region_map_[end-1]+1); +} + +template void MemoryMap::set_paging() { + // Establish whether main or auxiliary RAM + // is exposed in bank $00 for a bunch of regions. + if constexpr (type & PagingType::Main) { + const auto set = [&](std::size_t page, const auto &flags) { + auto ®ion = regions_[region_map_[page]]; + region.read = flags.read ? &ram_base_[0x01'0000] : ram_base_; + region.write = flags.write ? &ram_base_[0x01'0000] : ram_base_; + }; + const auto state = auxiliary_switches_.main_state(); + + // Base: $0200–$03FF. + set(0x02, state.base); + assert_is_region(0x02, 0x04); + + // Region $0400–$07ff. + set(0x04, state.region_04_08); + assert_is_region(0x04, 0x08); + + // Base: $0800–$1FFF. + set(0x08, state.base); + assert_is_region(0x08, 0x20); + + // Region $2000–$3FFF. + set(0x20, state.region_20_40); + assert_is_region(0x20, 0x40); + + // Base: $4000–$BFFF. + set(0x40, state.base); + assert_is_region(0x40, 0xc0); + } + + // Update whether base or auxiliary RAM is visible in: (i) the zero + // and stack pages; and (ii) anywhere that the language card is exposing RAM instead of ROM. + if constexpr (bool(type & PagingType::ZeroPage)) { + // Affects bank $00 only, and should be a single region. + auto ®ion = regions_[region_map_[0]]; + region.read = region.write = auxiliary_switches_.zero_state() ? &ram_base_[0x01'0000] : ram_base_; + assert(region_map_[0x0000] == region_map_[0x0001]); + assert(region_map_[0x0001]+1 == region_map_[0x0002]); + } + + // Establish whether ROM or card switches are exposed in the distinct + // regions C100–C2FF, C300–C3FF, C400–C7FF and C800–CFFF. + // + // On the IIgs it intersects with the current shadow register. + if constexpr (bool(type & (PagingType::CardArea | PagingType::Main))) { + const bool inhibit_banks0001 = shadow_register_ & 0x40; + const auto state = auxiliary_switches_.card_state(); + + auto apply = [&state, this](uint32_t bank_base) { + auto &c0_region = regions_[region_map_[bank_base | 0xc0]]; + auto &c1_region = regions_[region_map_[bank_base | 0xc1]]; + auto &c3_region = regions_[region_map_[bank_base | 0xc3]]; + auto &c4_region = regions_[region_map_[bank_base | 0xc4]]; + auto &c8_region = regions_[region_map_[bank_base | 0xc8]]; + + const uint8_t *const rom = ®ions_[region_map_[0xffd0]].read[0xffc100] - ((bank_base << 8) + 0xc100); + + // This is applied dynamically as it may be added or lost in banks $00 and $01. + c0_region.flags |= Region::IsIO; + + const auto apply_region = [&](bool flag, auto ®ion) { + region.write = nullptr; + if(flag) { + region.read = rom; + region.flags &= ~Region::IsIO; + } else { + region.flags |= Region::IsIO; + } + }; + + apply_region(state.region_C1_C3, c1_region); + apply_region(state.region_C3, c3_region); + apply_region(state.region_C4_C8, c4_region); + apply_region(state.region_C8_D0, c8_region); + + // Sanity checks. + assert(region_map_[bank_base | 0xc1] == region_map_[bank_base | 0xc0]+1); + assert(region_map_[bank_base | 0xc2] == region_map_[bank_base | 0xc1]); + assert(region_map_[bank_base | 0xc3] == region_map_[bank_base | 0xc2]+1); + assert(region_map_[bank_base | 0xc4] == region_map_[bank_base | 0xc3]+1); + assert(region_map_[bank_base | 0xc7] == region_map_[bank_base | 0xc4]); + assert(region_map_[bank_base | 0xc8] == region_map_[bank_base | 0xc7]+1); + assert(region_map_[bank_base | 0xcf] == region_map_[bank_base | 0xc8]); + assert(region_map_[bank_base | 0xd0] == region_map_[bank_base | 0xcf]+1); + }; + + if(inhibit_banks0001) { + // Set no IO in the Cx00 range for banks $00 and $01, just + // regular RAM (or possibly auxiliary). + const auto auxiliary_state = auxiliary_switches_.main_state(); + for(uint8_t region = region_map_[0x00c0]; region < region_map_[0x00d0]; region++) { + regions_[region].read = auxiliary_state.base.read ? &ram_base_[0x01'0000] : ram_base_; + regions_[region].write = auxiliary_state.base.write ? &ram_base_[0x01'0000] : ram_base_; + regions_[region].flags &= ~Region::IsIO; + } + for(uint8_t region = region_map_[0x01c0]; region < region_map_[0x01d0]; region++) { + regions_[region].read = regions_[region].write = ram_base_; + regions_[region].flags &= ~Region::IsIO; + } + } else { + // Obey the card state for banks $00 and $01. + apply(0x0000); + apply(0x0100); + } + + // Obey the card state for banks $e0 and $e1. + apply(0xe000); + apply(0xe100); + } + + // Update the region from + // $D000 onwards as per the state of the language card flags — there may + // end up being ROM or RAM (or auxiliary RAM), and the first 4kb of it + // may be drawn from either of two pools. + if constexpr (bool(type & (PagingType::LanguageCard | PagingType::ZeroPage | PagingType::Main))) { + const auto language_state = language_card_.state(); + const auto zero_state = auxiliary_switches_.zero_state(); + const auto main = auxiliary_switches_.main_state(); + const bool inhibit_banks0001 = shadow_register_ & 0x40; + + auto apply = [&language_state, this](uint32_t bank_base, uint8_t *ram) { + // This assumes bank 1 is the one before bank 2 when RAM is linear. + uint8_t *const d0_ram_bank = ram - (language_state.bank2 ? 0x0000 : 0x1000); + + // Crib the ROM pointer from a page it's always visible on. + const uint8_t *const rom = ®ions_[region_map_[0xffd0]].read[0xff'd000] - ((bank_base << 8) + 0xd000); + + auto &d0_region = regions_[region_map_[bank_base | 0xd0]]; + d0_region.read = language_state.read ? d0_ram_bank : rom; + d0_region.write = language_state.write ? nullptr : d0_ram_bank; + + auto &e0_region = regions_[region_map_[bank_base | 0xe0]]; + e0_region.read = language_state.read ? ram : rom; + e0_region.write = language_state.write ? nullptr : ram; + + // Assert assumptions made above re: memory layout. + assert(region_map_[bank_base | 0xd0] + 1 == region_map_[bank_base | 0xe0]); + assert(region_map_[bank_base | 0xe0] == region_map_[bank_base | 0xff]); + }; + auto set_no_card = [this](uint32_t bank_base, uint8_t *read, uint8_t *write) { + auto &d0_region = regions_[region_map_[bank_base | 0xd0]]; + d0_region.read = read; + d0_region.write = write; + + auto &e0_region = regions_[region_map_[bank_base | 0xe0]]; + e0_region.read = read; + e0_region.write = write; + + // Assert assumptions made above re: memory layout. + assert(region_map_[bank_base | 0xd0] + 1 == region_map_[bank_base | 0xe0]); + assert(region_map_[bank_base | 0xe0] == region_map_[bank_base | 0xff]); + }; + + if(inhibit_banks0001) { + set_no_card(0x0000, + main.base.read ? &ram_base_[0x01'0000] : ram_base_, + main.base.write ? &ram_base_[0x01'0000] : ram_base_); + set_no_card(0x0100, ram_base_, ram_base_); + } else { + apply(0x0000, zero_state ? &ram_base_[0x01'0000] : ram_base_); + apply(0x0100, ram_base_); + } + + // The pointer stored in region_map_[0xe000] has already been adjusted for + // the 0xe0'0000 addressing offset. + uint8_t *const e0_ram = regions_[region_map_[0xe000]].write; + apply(0xe000, e0_ram); + apply(0xe100, e0_ram); + } +} + +// IIgs specific: sets or resets the ::IsShadowed flag across affected banks as +// per the current state of the shadow register. +// +// Completely distinct from the auxiliary and language card switches. +void MemoryMap::set_shadowing() { + // Relevant bits: + // + // b5: inhibit shadowing, text page 2 [if ROM 03; as if always set otherwise] + // b4: inhibit shadowing, auxiliary high-res graphics + // b3: inhibit shadowing, super high-res graphics + // b2: inhibit shadowing, high-res graphics page 2 + // b1: inhibit shadowing, high-res graphics page 1 + // b0: inhibit shadowing, text page 1 + // + // The interpretations of how the overlapping high-res and super high-res inhibit + // bits apply used below is taken from The Apple IIgs Technical Reference, P. 178. + + // Of course, zones are: + // + // $0400–$0800 Text Page 1 + // $0800–$0C00 Text Page 2 [ROM 03 machines] + // $2000–$4000 High-res Page 1, and Super High-res in odd banks + // $4000–$6000 High-res Page 2, and Huper High-res in odd banks + // $6000–$a000 Odd banks only, rest of Super High-res + // [plus IO and language card space, subject to your definition of shadowing] + + enum Inhibit { + TextPage1 = 0x01, + HighRes1 = 0x02, + HighRes2 = 0x04, + SuperHighRes = 0x08, + AuxiliaryHighRes = 0x10, + TextPage2 = 0x20, + }; + + // Clear all shadowing. + shadow_pages_.reset(); + + // Text Page 1, main and auxiliary — $0400–$0800. + { + const bool should_shadow_text1 = !(shadow_register_ & Inhibit::TextPage1); + if(should_shadow_text1) { + shadow_pages_ |= shadow_text1_; + } + } + + // Text Page 2, main and auxiliary — 0x0800–0x0c00. + // + // The mask applied will be all 0 for a pre-ROM03 machine. + { + const bool should_shadow_text2 = !(shadow_register_ & Inhibit::TextPage2); + if(should_shadow_text2) { + shadow_pages_ |= shadow_text2_; + } + } + + // Hi-res graphics Page 1, main and auxiliary — $2000–$4000; + // also part of the super high-res graphics page on odd pages. + // + // Even test applied: + // high-res graphics page 1 inhibit bit alone is definitive. + // + // Odd test: + // (high-res graphics inhibit or auxiliary high res graphics inhibit) _and_ + // (super high-res inhibit). + // + { + const bool should_shadow_highres1 = !(shadow_register_ & Inhibit::HighRes1); + if(should_shadow_highres1) { + shadow_pages_ |= shadow_highres1_; + } + + const bool should_shadow_aux_highres1 = !( + shadow_register_ & (Inhibit::HighRes1 | Inhibit::AuxiliaryHighRes) && + shadow_register_ & Inhibit::SuperHighRes + ); + if(should_shadow_aux_highres1) { + shadow_pages_ |= shadow_highres1_aux_; + } + } + + // Hi-res graphics Page 2, main and auxiliary — $4000–$6000; + // also part of the super high-res graphics page. + // + // Test applied: much like that for page 1. + { + const bool should_shadow_highres2 = !(shadow_register_ & Inhibit::HighRes2); + if(should_shadow_highres2) { + shadow_pages_ |= shadow_highres2_; + } + + const bool should_shadow_aux_highres2 = !( + shadow_register_ & (Inhibit::HighRes2 | Inhibit::AuxiliaryHighRes) && + shadow_register_ & Inhibit::SuperHighRes + ); + if(should_shadow_aux_highres2) { + shadow_pages_ |= shadow_highres2_aux_; + } + } + + // Residue of Super Hi-Res — $6000–$a000 (odd pages only). + // + // Test applied: + // auxiliary high res graphics inhibit and super high-res inhibit + { + const bool should_shadow_superhighres = !( + shadow_register_ & Inhibit::SuperHighRes && + shadow_register_ & Inhibit::AuxiliaryHighRes + ); + if(should_shadow_superhighres) { + shadow_pages_ |= shadow_superhighres_; + } + } +} + +void MemoryMap::setup_shadow_maps(bool is_rom03) { + static constexpr int shadow_shift = 10; + static constexpr int auxiliary_offset = 0x1'0000 >> shadow_shift; + + for(size_t c = 0x0400 >> shadow_shift; c < 0x0800 >> shadow_shift; c++) { + shadow_text1_[c] = shadow_text1_[c+auxiliary_offset] = true; + } + + // Shadowing of text page 2 was added only with the ROM03 machine. + if(is_rom03) { + for(size_t c = 0x0800 >> shadow_shift; c < 0x0c00 >> shadow_shift; c++) { + shadow_text2_[c] = shadow_text2_[c+auxiliary_offset] = true; + } + } + + for(size_t c = 0x2000 >> shadow_shift; c < 0x4000 >> shadow_shift; c++) { + shadow_highres1_[c] = true; + shadow_highres1_aux_[c+auxiliary_offset] = true; + } + + for(size_t c = 0x4000 >> shadow_shift; c < 0x6000 >> shadow_shift; c++) { + shadow_highres2_[c] = true; + shadow_highres2_aux_[c+auxiliary_offset] = true; + } + + for(size_t c = 0x6000 >> shadow_shift; c < 0xa000 >> shadow_shift; c++) { + shadow_superhighres_[c+auxiliary_offset] = true; + } +} diff --git a/Machines/Apple/AppleIIgs/MemoryMap.hpp b/Machines/Apple/AppleIIgs/MemoryMap.hpp index f52b98dad..795cc9e27 100644 --- a/Machines/Apple/AppleIIgs/MemoryMap.hpp +++ b/Machines/Apple/AppleIIgs/MemoryMap.hpp @@ -6,23 +6,19 @@ // Copyright © 2020 Thomas Harte. All rights reserved. // -#ifndef Machines_Apple_AppleIIgs_MemoryMap_hpp -#define Machines_Apple_AppleIIgs_MemoryMap_hpp +#pragma once #include #include +#include #include #include "../AppleII/LanguageCardSwitches.hpp" #include "../AppleII/AuxiliaryMemorySwitches.hpp" -namespace Apple { -namespace IIgs { +namespace Apple::IIgs { class MemoryMap { - private: - using PagingType = Apple::II::PagingType; - public: // MARK: - Initial construction and configuration. @@ -30,204 +26,20 @@ class MemoryMap { setup_shadow_maps(is_rom03); } - void set_storage(std::vector &ram, std::vector &rom) { - // Keep a pointer for later; also note the proper RAM offset. - ram_base = ram.data(); - shadow_base[0] = ram_base; // i.e. all unshadowed writes go to where they've already gone (to make a no-op). - shadow_base[1] = &ram[ram.size() - 0x02'0000]; // i.e. all shadowed writes go somewhere in the last - // 128bk of RAM. - - // Establish bank mapping. - uint8_t next_region = 0; - auto region = [&next_region, this]() -> uint8_t { - assert(next_region != this->regions.size()); - return next_region++; - }; - auto set_region = [this](uint8_t bank, uint16_t start, uint16_t end, uint8_t region) { - assert((end == 0xffff) || !(end&0xff)); - assert(!(start&0xff)); - - // Fill in memory map. - size_t target = size_t((bank << 8) | (start >> 8)); - for(int c = start; c < end; c += 0x100) { - region_map[target] = region; - ++target; - } - }; - auto set_regions = [set_region, region](uint8_t bank, std::initializer_list addresses, std::vector allocated_regions = {}) { - uint16_t previous = 0x0000; - auto next_region = allocated_regions.begin(); - for(uint16_t address: addresses) { - set_region(bank, previous, address, next_region != allocated_regions.end() ? *next_region : region()); - previous = address; - assert(next_region != allocated_regions.end() || allocated_regions.empty()); - if(next_region != allocated_regions.end()) ++next_region; - } - assert(next_region == allocated_regions.end()); - }; - - // Current beliefs about the IIgs memory map: - // - // * language card banking applies to banks $00, $01, $e0 and $e1; - // * auxiliary memory switches apply to bank $00 only; - // * shadowing may be enabled only on banks $00 and $01, or on all RAM pages; and - // * whether bit 16 of the address is passed to the Mega II is selectable — this affects both the destination - // of odd-bank shadows, and whether bank $e1 is actually distinct from $e0. - // - // So: - // - // * bank $00 needs to be divided by auxiliary and language card zones; - // * banks $01, $e0 and $e1 need to be divided by language card zones only; and - // * ROM banks and all other fast RAM banks don't need subdivision. - - // Language card zones: - // - // $D000–$E000 4kb window, into either bank 1 or bank 2 - // $E000–end 12kb window, always the same RAM. - - // Auxiliary zones: - // - // $0000–$0200 Zero page (and stack) - // $0200–$0400 [space in between] - // $0400–$0800 Text Page 1 - // $0800–$2000 [space in between] - // $2000–$4000 High-res Page 1 - // $4000–$C000 [space in between] - - // Card zones: - // - // $C100–$C2FF either cards or IIe-style ROM - // $C300–$C3FF IIe-supplied 80-column card replacement ROM - // $C400–$C7FF either cards or IIe-style ROM - // $C800–$CFFF Standard extended card area - - // Reserve region 0 as that for unmapped memory. - region(); - - // Bank $00: all locations potentially affected by the auxiliary switches or the - // language switches. - set_regions(0x00, { - 0x0200, 0x0400, 0x0800, - 0x2000, 0x4000, - 0xc000, 0xc100, 0xc300, 0xc400, 0xc800, - 0xd000, 0xe000, - 0xffff - }); - - // Bank $01: all locations potentially affected by the language switches and card switches. - set_regions(0x01, { - 0xc000, 0xc100, 0xc300, 0xc400, 0xc800, - 0xd000, 0xe000, - 0xffff - }); - - // Banks $02–[end of RAM]: a single region. - const auto fast_region = region(); - const uint8_t fast_ram_bank_limit = uint8_t(ram.size() / 0x01'0000); - for(uint8_t bank = 0x02; bank < fast_ram_bank_limit; bank++) { - set_region(bank, 0x0000, 0xffff, fast_region); - } - - // [Banks $80–$e0: empty]. - - // Banks $e0, $e1: all locations potentially affected by the language switches or marked for IO. - // Alas, separate regions are needed due to the same ROM appearing on both pages. - for(uint8_t c = 0; c < 2; c++) { - set_regions(0xe0 + c, {0xc000, 0xc100, 0xc300, 0xc400, 0xc800, 0xd000, 0xe000, 0xffff}); - } - - // [Banks $e2–[ROM start]: empty]. - - // ROM banks: directly mapped to ROM. - const uint8_t rom_bank_count = uint8_t(rom.size() >> 16); - const uint8_t first_rom_bank = uint8_t(0x100 - rom_bank_count); - const uint8_t rom_region = region(); - for(uint8_t c = 0; c < rom_bank_count; ++c) { - set_region(first_rom_bank + c, 0x0000, 0xffff, rom_region); - } - - // Apply proper storage to those banks. - auto set_storage = [this](uint32_t address, const uint8_t *read, uint8_t *write) { - // Don't allow the reserved null region to be modified. - assert(region_map[address >> 8]); - - // Either set or apply a quick bit of testing as to the logic at play. - auto ®ion = regions[region_map[address >> 8]]; - if(read) read -= address; - if(write) write -= address; - if(!region.read) { - region.read = read; - region.write = write; - } else { - assert(region.read == read); - assert(region.write == write); - } - }; - - // This is highly redundant, but decouples this step from the above. - for(size_t c = 0; c < 0x80'0000; c += 0x100) { - if(c < ram.size() - 0x02'0000) { - set_storage(uint32_t(c), &ram[c], &ram[c]); - } - } - uint8_t *const slow_ram = &ram[ram.size() - 0x02'0000] - 0xe0'0000; - for(size_t c = 0xe0'0000; c < 0xe2'0000; c += 0x100) { - set_storage(uint32_t(c), &slow_ram[c], &slow_ram[c]); - } - for(uint32_t c = 0; c < uint32_t(rom_bank_count); c++) { - set_storage((first_rom_bank + c) << 16, &rom[c << 16], nullptr); - } - - // Set shadowing as working from banks 0 and 1 (forever). - shadow_banks[0] = true; - - // TODO: set 1Mhz flags. - - // Apply initial language/auxiliary state. - set_paging<~0>(); - } + /// Sets the ROM and RAM storage underlying this MemoryMap. + void set_storage(std::vector &ram, std::vector &rom); // MARK: - Live bus access notifications and register access. - void set_shadow_register(uint8_t value) { - const uint8_t diff = value ^ shadow_register_; - shadow_register_ = value; + void set_shadow_register(uint8_t value); + uint8_t get_shadow_register() const; - if(diff & 0x40) { // IO/language-card inhibit. - set_paging(); - } + void set_speed_register(uint8_t value); - if(diff & 0x3f) { - set_shadowing(); - } - } + void set_state_register(uint8_t value); + uint8_t get_state_register() const; - uint8_t get_shadow_register() const { - return shadow_register_; - } - - void set_speed_register(uint8_t value) { - speed_register_ = value; - - // Enable or disable shadowing from banks 0x02–0x80. - for(size_t c = 0x01; c < 0x40; c++) { - shadow_banks[c] = speed_register_ & 0x10; - } - } - - void set_state_register(uint8_t value) { - auxiliary_switches_.set_state(value); - language_card_.set_state(value); - } - - uint8_t get_state_register() const { - return language_card_.get_state() | auxiliary_switches_.get_state(); - } - - void access(uint16_t address, bool is_read) { - auxiliary_switches_.access(address, is_read); - if((address & 0xfff0) == 0xc080) language_card_.access(address, is_read); - } + void access(uint16_t address, bool is_read); using AuxiliaryMemorySwitches = Apple::II::AuxiliaryMemorySwitches; const AuxiliaryMemorySwitches &auxiliary_switches() const { @@ -239,383 +51,7 @@ class MemoryMap { return language_card_; } - private: - AuxiliaryMemorySwitches auxiliary_switches_; - LanguageCardSwitches language_card_; - friend AuxiliaryMemorySwitches; - friend LanguageCardSwitches; - - uint8_t shadow_register_ = 0x00; - uint8_t speed_register_ = 0x00; - - // MARK: - Memory banking. - -#define assert_is_region(start, end) \ - assert(region_map[start] == region_map[start-1]+1); \ - assert(region_map[end-1] == region_map[start]); \ - assert(region_map[end] == region_map[end-1]+1); - - template void set_paging() { - // Update the region from - // $D000 onwards as per the state of the language card flags — there may - // end up being ROM or RAM (or auxiliary RAM), and the first 4kb of it - // may be drawn from either of two pools. - if constexpr (bool(type & (PagingType::LanguageCard | PagingType::ZeroPage | PagingType::Main))) { - const auto language_state = language_card_.state(); - const auto zero_state = auxiliary_switches_.zero_state(); - const auto main = auxiliary_switches_.main_state(); - const bool inhibit_banks0001 = shadow_register_ & 0x40; - - auto apply = [&language_state, this](uint32_t bank_base, uint8_t *ram) { - // This assumes bank 1 is the one before bank 2 when RAM is linear. - uint8_t *const d0_ram_bank = ram - (language_state.bank2 ? 0x0000 : 0x1000); - - // Crib the ROM pointer from a page it's always visible on. - const uint8_t *const rom = ®ions[region_map[0xffd0]].read[0xff'd000] - ((bank_base << 8) + 0xd000); - - auto &d0_region = regions[region_map[bank_base | 0xd0]]; - d0_region.read = language_state.read ? d0_ram_bank : rom; - d0_region.write = language_state.write ? nullptr : d0_ram_bank; - - auto &e0_region = regions[region_map[bank_base | 0xe0]]; - e0_region.read = language_state.read ? ram : rom; - e0_region.write = language_state.write ? nullptr : ram; - - // Assert assumptions made above re: memory layout. - assert(region_map[bank_base | 0xd0] + 1 == region_map[bank_base | 0xe0]); - assert(region_map[bank_base | 0xe0] == region_map[bank_base | 0xff]); - }; - auto set_no_card = [this](uint32_t bank_base, uint8_t *read, uint8_t *write) { - auto &d0_region = regions[region_map[bank_base | 0xd0]]; - d0_region.read = read; - d0_region.write = write; - - auto &e0_region = regions[region_map[bank_base | 0xe0]]; - e0_region.read = read; - e0_region.write = write; - - // Assert assumptions made above re: memory layout. - assert(region_map[bank_base | 0xd0] + 1 == region_map[bank_base | 0xe0]); - assert(region_map[bank_base | 0xe0] == region_map[bank_base | 0xff]); - }; - - if(inhibit_banks0001) { - set_no_card(0x0000, - main.base.read ? &ram_base[0x01'0000] : ram_base, - main.base.write ? &ram_base[0x01'0000] : ram_base); - set_no_card(0x0100, ram_base, ram_base); - } else { - apply(0x0000, zero_state ? &ram_base[0x01'0000] : ram_base); - apply(0x0100, ram_base); - } - - // The pointer stored in region_map[0xe000] has already been adjusted for - // the 0xe0'0000 addressing offset. - uint8_t *const e0_ram = regions[region_map[0xe000]].write; - apply(0xe000, e0_ram); - apply(0xe100, e0_ram); - } - - // Establish whether main or auxiliary RAM - // is exposed in bank $00 for a bunch of regions. - if constexpr (type & PagingType::Main) { - const auto state = auxiliary_switches_.main_state(); - -#define set(page, flags) {\ - auto ®ion = regions[region_map[page]]; \ - region.read = flags.read ? &ram_base[0x01'0000] : ram_base; \ - region.write = flags.write ? &ram_base[0x01'0000] : ram_base; \ - } - - // Base: $0200–$03FF. - set(0x02, state.base); - assert_is_region(0x02, 0x04); - - // Region $0400–$07ff. - set(0x04, state.region_04_08); - assert_is_region(0x04, 0x08); - - // Base: $0800–$1FFF. - set(0x08, state.base); - assert_is_region(0x08, 0x20); - - // Region $2000–$3FFF. - set(0x20, state.region_20_40); - assert_is_region(0x20, 0x40); - - // Base: $4000–$BFFF. - set(0x40, state.base); - assert_is_region(0x40, 0xc0); - -#undef set - } - - // Update whether base or auxiliary RAM is visible in: (i) the zero - // and stack pages; and (ii) anywhere that the language card is exposing RAM instead of ROM. - if constexpr (bool(type & PagingType::ZeroPage)) { - // Affects bank $00 only, and should be a single region. - auto ®ion = regions[region_map[0]]; - region.read = region.write = auxiliary_switches_.zero_state() ? &ram_base[0x01'0000] : ram_base; - assert(region_map[0x0000] == region_map[0x0001]); - assert(region_map[0x0001]+1 == region_map[0x0002]); - } - - // Establish whether ROM or card switches are exposed in the distinct - // regions C100–C2FF, C300–C3FF, C400–C7FF and C800–CFFF. - // - // On the IIgs it intersects with the current shadow register. - if constexpr (bool(type & (PagingType::CardArea | PagingType::Main))) { - const bool inhibit_banks0001 = shadow_register_ & 0x40; - const auto state = auxiliary_switches_.card_state(); - - auto apply = [&state, this](uint32_t bank_base) { - auto &c0_region = regions[region_map[bank_base | 0xc0]]; - auto &c1_region = regions[region_map[bank_base | 0xc1]]; - auto &c3_region = regions[region_map[bank_base | 0xc3]]; - auto &c4_region = regions[region_map[bank_base | 0xc4]]; - auto &c8_region = regions[region_map[bank_base | 0xc8]]; - - const uint8_t *const rom = ®ions[region_map[0xffd0]].read[0xffc100] - ((bank_base << 8) + 0xc100); - - // This is applied dynamically as it may be added or lost in banks $00 and $01. - c0_region.flags |= Region::IsIO; - -#define apply_region(flag, region) \ - region.write = nullptr; \ - if(flag) { \ - region.read = rom; \ - region.flags &= ~Region::IsIO; \ - } else { \ - region.flags |= Region::IsIO; \ - } - - apply_region(state.region_C1_C3, c1_region); - apply_region(state.region_C3, c3_region); - apply_region(state.region_C4_C8, c4_region); - apply_region(state.region_C8_D0, c8_region); - -#undef apply_region - - // Sanity checks. - assert(region_map[bank_base | 0xc1] == region_map[bank_base | 0xc0]+1); - assert(region_map[bank_base | 0xc2] == region_map[bank_base | 0xc1]); - assert(region_map[bank_base | 0xc3] == region_map[bank_base | 0xc2]+1); - assert(region_map[bank_base | 0xc4] == region_map[bank_base | 0xc3]+1); - assert(region_map[bank_base | 0xc7] == region_map[bank_base | 0xc4]); - assert(region_map[bank_base | 0xc8] == region_map[bank_base | 0xc7]+1); - assert(region_map[bank_base | 0xcf] == region_map[bank_base | 0xc8]); - assert(region_map[bank_base | 0xd0] == region_map[bank_base | 0xcf]+1); - }; - - if(inhibit_banks0001) { - // Set no IO in the Cx00 range for banks $00 and $01, just - // regular RAM (or possibly auxiliary). - const auto auxiliary_state = auxiliary_switches_.main_state(); - for(uint8_t region = region_map[0x00c0]; region < region_map[0x00d0]; region++) { - regions[region].read = auxiliary_state.base.read ? &ram_base[0x01'0000] : ram_base; - regions[region].write = auxiliary_state.base.write ? &ram_base[0x01'0000] : ram_base; - regions[region].flags &= ~Region::IsIO; - } - for(uint8_t region = region_map[0x01c0]; region < region_map[0x01d0]; region++) { - regions[region].read = regions[region].write = ram_base; - regions[region].flags &= ~Region::IsIO; - } - } else { - // Obey the card state for banks $00 and $01. - apply(0x0000); - apply(0x0100); - } - - // Obey the card state for banks $e0 and $e1. - apply(0xe000); - apply(0xe100); - } - } - - // IIgs specific: sets or resets the ::IsShadowed flag across affected banks as - // per the current state of the shadow register. - // - // Completely distinct from the auxiliary and language card switches. - void set_shadowing() { - // Relevant bits: - // - // b5: inhibit shadowing, text page 2 [if ROM 03; as if always set otherwise] - // b4: inhibit shadowing, auxiliary high-res graphics - // b3: inhibit shadowing, super high-res graphics - // b2: inhibit shadowing, high-res graphics page 2 - // b1: inhibit shadowing, high-res graphics page 1 - // b0: inhibit shadowing, text page 1 - // - // The interpretations of how the overlapping high-res and super high-res inhibit - // bits apply used below is taken from The Apple IIgs Technical Reference, P. 178. - - // Of course, zones are: - // - // $0400–$0800 Text Page 1 - // $0800–$0C00 Text Page 2 [ROM 03 machines] - // $2000–$4000 High-res Page 1, and Super High-res in odd banks - // $4000–$6000 High-res Page 2, and Huper High-res in odd banks - // $6000–$a000 Odd banks only, rest of Super High-res - // [plus IO and language card space, subject to your definition of shadowing] - - enum Inhibit { - TextPage1 = 0x01, - HighRes1 = 0x02, - HighRes2 = 0x04, - SuperHighRes = 0x08, - AuxiliaryHighRes = 0x10, - TextPage2 = 0x20, - }; - - // Clear all shadowing. - shadow_pages.reset(); - - // Text Page 1, main and auxiliary — $0400–$0800. - { - const bool should_shadow_text1 = !(shadow_register_ & Inhibit::TextPage1); - if(should_shadow_text1) { - shadow_pages |= shadow_text1; - } - } - - // Text Page 2, main and auxiliary — 0x0800–0x0c00. - // - // The mask applied will be all 0 for a pre-ROM03 machine. - { - const bool should_shadow_text2 = !(shadow_register_ & Inhibit::TextPage2); - if(should_shadow_text2) { - shadow_pages |= shadow_text2; - } - } - - // Hi-res graphics Page 1, main and auxiliary — $2000–$4000; - // also part of the super high-res graphics page on odd pages. - // - // Even test applied: - // high-res graphics page 1 inhibit bit alone is definitive. - // - // Odd test: - // (high-res graphics inhibit or auxiliary high res graphics inhibit) _and_ - // (super high-res inhibit). - // - { - const bool should_shadow_highres1 = !(shadow_register_ & Inhibit::HighRes1); - if(should_shadow_highres1) { - shadow_pages |= shadow_highres1; - } - - const bool should_shadow_aux_highres1 = !( - shadow_register_ & (Inhibit::HighRes1 | Inhibit::AuxiliaryHighRes) && - shadow_register_ & Inhibit::SuperHighRes - ); - if(should_shadow_aux_highres1) { - shadow_pages |= shadow_highres1_aux; - } - } - - // Hi-res graphics Page 2, main and auxiliary — $4000–$6000; - // also part of the super high-res graphics page. - // - // Test applied: much like that for page 1. - { - const bool should_shadow_highres2 = !(shadow_register_ & Inhibit::HighRes2); - if(should_shadow_highres2) { - shadow_pages |= shadow_highres2; - } - - const bool should_shadow_aux_highres2 = !( - shadow_register_ & (Inhibit::HighRes2 | Inhibit::AuxiliaryHighRes) && - shadow_register_ & Inhibit::SuperHighRes - ); - if(should_shadow_aux_highres2) { - shadow_pages |= shadow_highres2_aux; - } - } - - // Residue of Super Hi-Res — $6000–$a000 (odd pages only). - // - // Test applied: - // auxiliary high res graphics inhibit and super high-res inhibit - { - const bool should_shadow_superhighres = !( - shadow_register_ & Inhibit::SuperHighRes && - shadow_register_ & Inhibit::AuxiliaryHighRes - ); - if(should_shadow_superhighres) { - shadow_pages |= shadow_superhighres; - } - } - } - - void print_state() { - uint8_t region = region_map[0]; - uint32_t start = 0; - for(uint32_t top = 0; top < 65536; top++) { - if(region_map[top] == region) continue; - - printf("%06x -> %06x\t", start, top << 8); - printf("%c%c\n", - (regions[region_map[top] - 1].flags & Region::Is1Mhz) ? '1' : '-', - (regions[region_map[top] - 1].flags & Region::IsIO) ? 'x' : '-' - ); - - start = top << 8; - region = region_map[top]; - } - } - -#undef assert_is_region - - private: - // Various precomputed bitsets describing key regions; std::bitset doesn't support constexpr instantiation - // beyond the first 64 bits at the time of writing, alas, so these are generated at runtime. - std::bitset<128> shadow_text1; - std::bitset<128> shadow_text2; - std::bitset<128> shadow_highres1, shadow_highres1_aux; - std::bitset<128> shadow_highres2, shadow_highres2_aux; - std::bitset<128> shadow_superhighres; - - void setup_shadow_maps(bool is_rom03) { - static constexpr int shadow_shift = 10; - static constexpr int auxiliary_offset = 0x1'0000 >> shadow_shift; - - for(size_t c = 0x0400 >> shadow_shift; c < 0x0800 >> shadow_shift; c++) { - shadow_text1[c] = shadow_text1[c+auxiliary_offset] = true; - } - - // Shadowing of text page 2 was added only with the ROM03 machine. - if(is_rom03) { - for(size_t c = 0x0800 >> shadow_shift; c < 0x0c00 >> shadow_shift; c++) { - shadow_text2[c] = shadow_text2[c+auxiliary_offset] = true; - } - } - - for(size_t c = 0x2000 >> shadow_shift; c < 0x4000 >> shadow_shift; c++) { - shadow_highres1[c] = true; - shadow_highres1_aux[c+auxiliary_offset] = true; - } - - for(size_t c = 0x4000 >> shadow_shift; c < 0x6000 >> shadow_shift; c++) { - shadow_highres2[c] = true; - shadow_highres2_aux[c+auxiliary_offset] = true; - } - - for(size_t c = 0x6000 >> shadow_shift; c < 0xa000 >> shadow_shift; c++) { - shadow_superhighres[c+auxiliary_offset] = true; - } - } - - public: - // Memory layout here is done via double indirection; the main loop should: - // (i) use the top two bytes of the address to get an index from memory_map_; and - // (ii) use that to index the memory_regions table. - // - // Pointers are eight bytes at the time of writing, so the extra level of indirection - // reduces what would otherwise be a 1.25mb table down to not a great deal more than 64kb. - std::array region_map{}; - uint8_t *ram_base = nullptr; - uint8_t *shadow_base[2] = {nullptr, nullptr}; - static constexpr int shadow_mask[2] = {0xff'ffff, 0x01'ffff}; + // MARK: - Accessors for reading and writing RAM. struct Region { uint8_t *write = nullptr; @@ -628,66 +64,99 @@ class MemoryMap { }; }; - // Shadow_pages: divides the final 128kb of memory into 1kb chunks and includes a flag to indicate whether - // each is a potential destination for shadowing. - // - // Shadow_banks: divides the whole 16mb of memory into 128kb chunks and includes a flag to indicate whether - // each is a potential source of shadowing. - std::bitset<128> shadow_pages{}, shadow_banks{}; + const Region ®ion(uint32_t address) const { return regions_[region_map_[address >> 8]]; } + uint8_t read(const Region ®ion, uint32_t address) const { + return region.read ? region.read[address] : 0xff; + } - std::array regions; // An assert above ensures that this is large enough; there's no - // doctrinal reason for it to be whatever size it is now, just - // adjust as required. + bool is_shadowed(const Region ®ion, uint32_t address) const { + // ROM is never shadowed. + if(!region.write) { + return false; + } + + const auto physical = physical_address(region, address); + assert(physical >= 0 && physical <= 0xff'ffff); + return shadow_pages_[(physical >> 10) & 127] & shadow_banks_[physical >> 17]; + } + void write(const Region ®ion, uint32_t address, uint8_t value) { + if(!region.write) { + return; + } + + // Write once. + region.write[address] = value; + + // Write again, either to the same place (if unshadowed) or to the shadow destination. + static constexpr std::size_t shadow_mask[2] = {0xff'ffff, 0x01'ffff}; + const bool shadowed = is_shadowed(region, address); + shadow_base_[shadowed][physical_address(region, address) & shadow_mask[shadowed]] = value; + } + + // The objective is to support shadowing: + // 1. without storing a whole extra pointer, and such that the shadowing flags + // are orthogonal to the current auxiliary memory settings; + // 2. in such a way as to support shadowing both in banks $00/$01 and elsewhere; and + // 3. to do so without introducing too much in the way of branching. + // + // Hence the implemented solution: if shadowing is enabled then use the distance from the start of + // physical RAM modulo 128k indexed into the bank $e0/$e1 RAM. + // + // With a further twist: the modulo and pointer are indexed on ::IsShadowed to eliminate a branch + // even on that. + + private: + AuxiliaryMemorySwitches auxiliary_switches_; + LanguageCardSwitches language_card_; + friend AuxiliaryMemorySwitches; + friend LanguageCardSwitches; + + uint8_t shadow_register_ = 0x00; + uint8_t speed_register_ = 0x00; + + // MARK: - Banking. + + void assert_is_region(uint8_t start, uint8_t end); + template void set_paging(); + + uint8_t *ram_base_ = nullptr; + + // Memory layout here is done via double indirection; the main loop should: + // (i) use the top two bytes of the address to get an index from region_map; and + // (ii) use that to index the memory_regions table. + // + // Pointers are eight bytes at the time of writing, so the extra level of indirection + // reduces what would otherwise be a 1.25mb table down to not a great deal more than 64kb. + std::array region_map_{}; + std::array regions_; // An assert above ensures that this is large enough; there's no + // doctrinal reason for it to be whatever size it is now, just + // adjust as required. + + std::size_t physical_address(const Region ®ion, uint32_t address) const { + return std::size_t(®ion.write[address] - ram_base_); + } + + // MARK: - Shadowing + + // Various precomputed bitsets describing key regions; std::bitset doesn't support constexpr instantiation + // beyond the first 64 bits at the time of writing, alas, so these are generated at runtime. + std::bitset<128> shadow_text1_; + std::bitset<128> shadow_text2_; + std::bitset<128> shadow_highres1_, shadow_highres1_aux_; + std::bitset<128> shadow_highres2_, shadow_highres2_aux_; + std::bitset<128> shadow_superhighres_; + void setup_shadow_maps(bool is_rom03); + void set_shadowing(); + + uint8_t *shadow_base_[2] = {nullptr, nullptr}; + + // Divide the final 128kb of memory into 1kb chunks and flag to indicate whether + // each is a potential destination for shadowing. + std::bitset<128> shadow_pages_{}; + + // Divide the whole 16mb of memory into 128kb chunks and flag to indicate whether + // each is a potential source of shadowing. + std::bitset<128> shadow_banks_{}; }; -// TODO: branching below on region.read/write is predicated on the idea that extra scratch space -// would be less efficient. Verify that? - -#define MemoryMapRegion(map, address) map.regions[map.region_map[address >> 8]] -#define MemoryMapRead(region, address, value) *value = region.read ? region.read[address] : 0xff - -// The below encapsulates the fact that I've yet to determine whether Apple intends to -// indicate that logical addresses (i.e. those prior to being mapped per the current paging) -// or physical addresses (i.e. after mapping) are subject to shadowing. -#ifdef SHADOW_LOGICAL - -#define IsShadowed(map, region, address) \ - (map.shadow_pages[((®ion.write[address] - map.ram_base) >> 10) & 127] & map.shadow_banks[address >> 17]) - -#define MemoryMapWrite(map, region, address, value) \ - if(region.write) { \ - region.write[address] = *value; \ - const bool _mm_is_shadowed = IsShadowed(map, region, address); \ - map.shadow_base[_mm_is_shadowed][address & map.shadow_mask[_mm_is_shadowed]] = *value; \ - } - -#else - -#define IsShadowed(map, region, address) \ - (map.shadow_pages[(address >> 10) & 127] & map.shadow_banks[address >> 17]) - -#define MemoryMapWrite(map, region, address, value) \ - if(region.write) { \ - region.write[address] = *value; \ - const bool _mm_is_shadowed = IsShadowed(map, region, address); \ - map.shadow_base[_mm_is_shadowed][(®ion.write[address] - map.ram_base) & map.shadow_mask[_mm_is_shadowed]] = *value; \ - } - -#endif - -// Quick notes on ::IsShadowed contortions: -// -// The objective is to support shadowing: -// 1. without storing a whole extra pointer, and such that the shadowing flags are orthogonal to the current auxiliary memory settings; -// 2. in such a way as to support shadowing both in banks $00/$01 and elsewhere; and -// 3. to do so without introducing too much in the way of branching. -// -// Hence the implemented solution: if shadowing is enabled then use the distance from the start of physical RAM -// modulo 128k indexed into the bank $e0/$e1 RAM. -// -// With a further twist: the modulo and pointer are indexed on ::IsShadowed to eliminate a branch even on that. - } -} - -#endif /* MemoryMap_h */ diff --git a/Machines/Apple/AppleIIgs/Sound.cpp b/Machines/Apple/AppleIIgs/Sound.cpp index f1599b1eb..0a5c78869 100644 --- a/Machines/Apple/AppleIIgs/Sound.cpp +++ b/Machines/Apple/AppleIIgs/Sound.cpp @@ -218,7 +218,7 @@ uint8_t GLU::get_address_high() { // MARK: - Update logic. -Cycles GLU::get_next_sequence_point() const { +Cycles GLU::next_sequence_point() const { uint32_t result = std::numeric_limits::max(); for(int c = 0; c < local_.oscillator_count; c++) { diff --git a/Machines/Apple/AppleIIgs/Sound.hpp b/Machines/Apple/AppleIIgs/Sound.hpp index 942a94b7d..55c4f4c20 100644 --- a/Machines/Apple/AppleIIgs/Sound.hpp +++ b/Machines/Apple/AppleIIgs/Sound.hpp @@ -6,8 +6,7 @@ // Copyright © 2020 Thomas Harte. All rights reserved. // -#ifndef Apple_IIgs_Sound_hpp -#define Apple_IIgs_Sound_hpp +#pragma once #include @@ -15,9 +14,7 @@ #include "../../../Concurrency/AsyncTaskQueue.hpp" #include "../../../Outputs/Speaker/Implementation/SampleSource.hpp" -namespace Apple { -namespace IIgs { -namespace Sound { +namespace Apple::IIgs::Sound { class GLU: public Outputs::Speaker::SampleSource { public: @@ -33,7 +30,7 @@ class GLU: public Outputs::Speaker::SampleSource { uint8_t get_address_high(); void run_for(Cycles); - Cycles get_next_sequence_point() const; + Cycles next_sequence_point() const; bool get_interrupt_line(); // SampleSource. @@ -105,7 +102,3 @@ class GLU: public Outputs::Speaker::SampleSource { }; } -} -} - -#endif /* SoundGLU_hpp */ diff --git a/Machines/Apple/AppleIIgs/Video.cpp b/Machines/Apple/AppleIIgs/Video.cpp index e78e6ff4b..ffacbdb5e 100644 --- a/Machines/Apple/AppleIIgs/Video.cpp +++ b/Machines/Apple/AppleIIgs/Video.cpp @@ -22,31 +22,31 @@ constexpr auto FinalColumn = CyclesPerLine / CyclesPerTick; // Converts from Apple's RGB ordering to this emulator's. #if TARGET_RT_BIG_ENDIAN -#define PaletteConvulve(x) uint16_t(x) +constexpr uint16_t convulve(uint16_t x) { return x; } #else -#define PaletteConvulve(x) uint16_t(((x&0xf00) >> 8) | ((x&0x0ff) << 8)) +constexpr uint16_t convulve(uint16_t x) { return uint16_t(((x&0xf00) >> 8) | ((x&0x0ff) << 8)); } #endif // The 12-bit values used by the Apple IIgs to approximate Apple II colours, // as implied by tech note #63's use of them as border colours. // http://www.1000bit.it/support/manuali/apple/technotes/iigs/tn.iigs.063.html constexpr uint16_t appleii_palette[16] = { - PaletteConvulve(0x0000), // Black. - PaletteConvulve(0x0d03), // Deep Red. - PaletteConvulve(0x0009), // Dark Blue. - PaletteConvulve(0x0d2d), // Purple. - PaletteConvulve(0x0072), // Dark Green. - PaletteConvulve(0x0555), // Dark Gray. - PaletteConvulve(0x022f), // Medium Blue. - PaletteConvulve(0x06af), // Light Blue. - PaletteConvulve(0x0850), // Brown. - PaletteConvulve(0x0f60), // Orange. - PaletteConvulve(0x0aaa), // Light Grey. - PaletteConvulve(0x0f98), // Pink. - PaletteConvulve(0x01d0), // Light Green. - PaletteConvulve(0x0ff0), // Yellow. - PaletteConvulve(0x04f9), // Aquamarine. - PaletteConvulve(0x0fff), // White. + convulve(0x0000), // Black. + convulve(0x0d03), // Deep Red. + convulve(0x0009), // Dark Blue. + convulve(0x0d2d), // Purple. + convulve(0x0072), // Dark Green. + convulve(0x0555), // Dark Gray. + convulve(0x022f), // Medium Blue. + convulve(0x06af), // Light Blue. + convulve(0x0850), // Brown. + convulve(0x0f60), // Orange. + convulve(0x0aaa), // Light Grey. + convulve(0x0f98), // Pink. + convulve(0x01d0), // Light Green. + convulve(0x0ff0), // Yellow. + convulve(0x04f9), // Aquamarine. + convulve(0x0fff), // White. }; // Reasoned guesswork ahoy! @@ -191,7 +191,7 @@ void Video::advance(Cycles cycles) { } } -Cycles Video::get_next_sequence_point() const { +Cycles Video::next_sequence_point() const { const int cycles_into_row = cycles_into_frame_ % CyclesPerLine; const int row = cycles_into_frame_ / CyclesPerLine; @@ -279,13 +279,16 @@ void Video::output_row(int row, int start, int end) { line_control_ = ram_[0x19d00 + row]; const int palette_base = (line_control_ & 15) * 32 + 0x19e00; for(int c = 0; c < 16; c++) { - const int entry = ram_[palette_base + (c << 1)] | (ram_[palette_base + (c << 1) + 1] << 8); - palette_[c] = PaletteConvulve(entry); + const auto entry = uint16_t( + ram_[palette_base + (c << 1)] | + (ram_[palette_base + (c << 1) + 1] << 8) + ); + palette_[c] = convulve(entry); } // Post an interrupt if requested. if(line_control_ & 0x40) { - set_interrupts(0x20); + interrupts_.add(0x20); } // Set up appropriately for fill mode (or not). @@ -498,19 +501,19 @@ uint8_t Video::get_new_video() { } void Video::clear_interrupts(uint8_t mask) { - set_interrupts(interrupts_ & ~(mask & 0x60)); + interrupts_.clear(mask); } void Video::set_interrupt_register(uint8_t mask) { - set_interrupts(interrupts_ | (mask & 0x6)); + interrupts_.set_control(mask); } uint8_t Video::get_interrupt_register() { - return interrupts_; + return interrupts_.status(); } bool Video::get_interrupt_line() { - return (interrupts_&0x80) || (megaii_interrupt_mask_&megaii_interrupt_state_); + return interrupts_.active() || (megaii_interrupt_mask_ & megaii_interrupt_state_); } void Video::set_megaii_interrupts_enabled(uint8_t mask) { @@ -526,13 +529,7 @@ void Video::clear_megaii_interrupts() { } void Video::notify_clock_tick() { - set_interrupts(interrupts_ | 0x40); -} - -void Video::set_interrupts(uint8_t new_value) { - interrupts_ = new_value & 0x7f; - if((interrupts_ >> 4) & interrupts_ & 0x6) - interrupts_ |= 0x80; + interrupts_.add(0x40); } void Video::set_border_colour(uint8_t colour) { @@ -635,21 +632,21 @@ uint16_t *Video::output_double_high_resolution_mono(uint16_t *target, int start, ram_[row_address + c], }; - target[0] = colours[(source[1] >> 0) & 0x1]; - target[1] = colours[(source[1] >> 1) & 0x1]; - target[2] = colours[(source[1] >> 2) & 0x1]; - target[3] = colours[(source[1] >> 3) & 0x1]; - target[4] = colours[(source[1] >> 4) & 0x1]; - target[5] = colours[(source[1] >> 5) & 0x1]; - target[6] = colours[(source[1] >> 6) & 0x1]; + target[0] = colours[(source[0] >> 0) & 0x1]; + target[1] = colours[(source[0] >> 1) & 0x1]; + target[2] = colours[(source[0] >> 2) & 0x1]; + target[3] = colours[(source[0] >> 3) & 0x1]; + target[4] = colours[(source[0] >> 4) & 0x1]; + target[5] = colours[(source[0] >> 5) & 0x1]; + target[6] = colours[(source[0] >> 6) & 0x1]; - target[7] = colours[(source[0] >> 0) & 0x1]; - target[8] = colours[(source[0] >> 1) & 0x1]; - target[9] = colours[(source[0] >> 2) & 0x1]; - target[10] = colours[(source[0] >> 3) & 0x1]; - target[11] = colours[(source[0] >> 4) & 0x1]; - target[12] = colours[(source[0] >> 5) & 0x1]; - target[13] = colours[(source[0] >> 6) & 0x1]; + target[7] = colours[(source[1] >> 0) & 0x1]; + target[8] = colours[(source[1] >> 1) & 0x1]; + target[9] = colours[(source[1] >> 2) & 0x1]; + target[10] = colours[(source[1] >> 3) & 0x1]; + target[11] = colours[(source[1] >> 4) & 0x1]; + target[12] = colours[(source[1] >> 5) & 0x1]; + target[13] = colours[(source[1] >> 6) & 0x1]; target += 14; } diff --git a/Machines/Apple/AppleIIgs/Video.hpp b/Machines/Apple/AppleIIgs/Video.hpp index d02e21d63..5219cffaa 100644 --- a/Machines/Apple/AppleIIgs/Video.hpp +++ b/Machines/Apple/AppleIIgs/Video.hpp @@ -6,16 +6,13 @@ // Copyright © 2020 Thomas Harte. All rights reserved. // -#ifndef Apple_IIgs_Video_hpp -#define Apple_IIgs_Video_hpp +#pragma once #include "../AppleII/VideoSwitches.hpp" #include "../../../Outputs/CRT/CRT.hpp" #include "../../../ClockReceiver/ClockReceiver.hpp" -namespace Apple { -namespace IIgs { -namespace Video { +namespace Apple::IIgs::Video { /*! Provides IIgs video output; assumed clocking here is seven times the usual Apple II clock. @@ -62,7 +59,7 @@ class Video: public Apple::II::VideoSwitches { Outputs::Display::DisplayType get_display_type() const; /// Determines the period until video might autonomously update its interrupt lines. - Cycles get_next_sequence_point() const; + Cycles next_sequence_point() const; /// Sets the Mega II interrupt enable state — 1/4-second and VBL interrupts are /// generated here. @@ -116,7 +113,7 @@ class Video: public Apple::II::VideoSwitches { switch(m) { case GraphicsMode::Text: return PixelBufferFormat::Text; case GraphicsMode::DoubleText: return PixelBufferFormat::DoubleText; - default: return PixelBufferFormat::NTSC; + default: return PixelBufferFormat::NTSC; case GraphicsMode::DoubleHighResMono: return PixelBufferFormat::NTSCMono; case GraphicsMode::SuperHighRes: return PixelBufferFormat::SuperHighRes; } @@ -125,8 +122,56 @@ class Video: public Apple::II::VideoSwitches { void advance(Cycles); uint8_t new_video_ = 0x01; - uint8_t interrupts_ = 0x00; - void set_interrupts(uint8_t); + + class Interrupts { + public: + void add(uint8_t value) { + // Taken literally, status accumulates regardless of being enabled, + // potentially to be polled, it simply doesn't trigger an interrupt. + value_ |= value; + test(); + } + + void clear(uint8_t value) { + // Zeroes in bits 5 or 6 clear the respective interrupts. + value_ &= value | ~0x60; + test(); + } + + void set_control(uint8_t value) { + // Ones in bits 1 or 2 enable the respective interrupts. + value_ = (value_ & ~0x6) | (value & 0x6); + test(); + } + + uint8_t status() const { + return value_; + } + + bool active() const { + return value_ & 0x80; + } + + private: + void test() { + value_ &= 0x7f; + if((value_ >> 4) & value_ & 0x6) { + value_ |= 0x80; + } + } + + // Overall meaning of value is as per the VGC interrupt register, i.e. + // + // b7: interrupt status; + // b6: 1-second interrupt status; + // b5: scan-line interrupt status; + // b4: reserved; + // b3: reserved; + // b2: 1-second interrupt enable; + // b1: scan-line interrupt enable; + // b0: reserved. + uint8_t value_ = 0x00; + } interrupts_; int cycles_into_frame_ = 0; const uint8_t *ram_ = nullptr; @@ -206,8 +251,3 @@ class Video: public Apple::II::VideoSwitches { }; } -} -} - -#endif /* Video_hpp */ - diff --git a/Machines/Apple/Macintosh/Audio.hpp b/Machines/Apple/Macintosh/Audio.hpp index bdf0ebd40..e132015e0 100644 --- a/Machines/Apple/Macintosh/Audio.hpp +++ b/Machines/Apple/Macintosh/Audio.hpp @@ -6,8 +6,7 @@ // Copyright © 2019 Thomas Harte. All rights reserved. // -#ifndef Audio_hpp -#define Audio_hpp +#pragma once #include "../../../Concurrency/AsyncTaskQueue.hpp" #include "../../../ClockReceiver/ClockReceiver.hpp" @@ -16,8 +15,7 @@ #include #include -namespace Apple { -namespace Macintosh { +namespace Apple::Macintosh { /*! Implements the Macintosh's audio output hardware. @@ -84,6 +82,3 @@ class Audio: public ::Outputs::Speaker::SampleSource { }; } -} - -#endif /* Audio_hpp */ diff --git a/Machines/Apple/Macintosh/DeferredAudio.hpp b/Machines/Apple/Macintosh/DeferredAudio.hpp index 9277e1569..95e13b3c1 100644 --- a/Machines/Apple/Macintosh/DeferredAudio.hpp +++ b/Machines/Apple/Macintosh/DeferredAudio.hpp @@ -6,14 +6,12 @@ // Copyright © 2019 Thomas Harte. All rights reserved. // -#ifndef DeferredAudio_h -#define DeferredAudio_h +#pragma once #include "Audio.hpp" #include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" -namespace Apple { -namespace Macintosh { +namespace Apple::Macintosh { struct DeferredAudio { Concurrency::AsyncTaskQueue queue; @@ -29,6 +27,3 @@ struct DeferredAudio { }; } -} - -#endif /* DeferredAudio_h */ diff --git a/Machines/Apple/Macintosh/DriveSpeedAccumulator.hpp b/Machines/Apple/Macintosh/DriveSpeedAccumulator.hpp index 45568e006..981f0b923 100644 --- a/Machines/Apple/Macintosh/DriveSpeedAccumulator.hpp +++ b/Machines/Apple/Macintosh/DriveSpeedAccumulator.hpp @@ -6,15 +6,13 @@ // Copyright © 2019 Thomas Harte. All rights reserved. // -#ifndef DriveSpeedAccumulator_hpp -#define DriveSpeedAccumulator_hpp +#pragma once #include #include #include -namespace Apple { -namespace Macintosh { +namespace Apple::Macintosh { class DriveSpeedAccumulator { public: @@ -41,6 +39,3 @@ class DriveSpeedAccumulator { }; } -} - -#endif /* DriveSpeedAccumulator_hpp */ diff --git a/Machines/Apple/Macintosh/Keyboard.hpp b/Machines/Apple/Macintosh/Keyboard.hpp index d69af1f56..2efd603c4 100644 --- a/Machines/Apple/Macintosh/Keyboard.hpp +++ b/Machines/Apple/Macintosh/Keyboard.hpp @@ -6,8 +6,7 @@ // Copyright © 2019 Thomas Harte. All rights reserved. // -#ifndef Apple_Macintosh_Keyboard_hpp -#define Apple_Macintosh_Keyboard_hpp +#pragma once #include "../../KeyboardMachine.hpp" #include "../../../ClockReceiver/ClockReceiver.hpp" @@ -15,8 +14,7 @@ #include #include -namespace Apple { -namespace Macintosh { +namespace Apple::Macintosh { constexpr uint16_t KeypadMask = 0x100; @@ -295,6 +293,3 @@ class KeyboardMapper: public MachineTypes::MappedKeyboardMachine::KeyboardMapper }; } -} - -#endif /* Apple_Macintosh_Keyboard_hpp */ diff --git a/Machines/Apple/Macintosh/Macintosh.cpp b/Machines/Apple/Macintosh/Macintosh.cpp index 02378af24..fd161b3a6 100644 --- a/Machines/Apple/Macintosh/Macintosh.cpp +++ b/Machines/Apple/Macintosh/Macintosh.cpp @@ -34,9 +34,8 @@ #include "../../../Components/AppleClock/AppleClock.hpp" #include "../../../Components/DiskII/IWM.hpp" #include "../../../Components/DiskII/MacintoshDoubleDensityDrive.hpp" -#include "../../../Processors/68000/68000.hpp" -#include "../../../Processors/68000Mk2/68000Mk2.hpp" +#include "../../../Processors/68000/68000.hpp" #include "../../../Storage/MassStorage/SCSI/SCSI.hpp" #include "../../../Storage/MassStorage/SCSI/DirectAccessDevice.hpp" @@ -51,7 +50,7 @@ namespace { constexpr int CLOCK_RATE = 7833600; constexpr auto KEYBOARD_CLOCK_RATE = HalfCycles(CLOCK_RATE / 100000); - +Log::Logger logger; // Former default PRAM: // @@ -74,7 +73,7 @@ template class ConcreteMachin public MachineTypes::MediaTarget, public MachineTypes::MouseMachine, public MachineTypes::MappedKeyboardMachine, - public CPU::MC68000Mk2::BusHandler, + public CPU::MC68000::BusHandler, public Zilog::SCC::z8530::Delegate, public Activity::Source, public Configurable::Device, @@ -89,17 +88,17 @@ template class ConcreteMachin Inputs::Keyboard::Key::LeftOption, Inputs::Keyboard::Key::RightOption, Inputs::Keyboard::Key::LeftMeta, Inputs::Keyboard::Key::RightMeta, }), - mc68000_(*this), - iwm_(CLOCK_RATE), - video_(audio_, drive_speed_accumulator_), - via_(via_port_handler_), - via_port_handler_(*this, clock_, keyboard_, audio_, iwm_, mouse_), - scsi_bus_(CLOCK_RATE * 2), - scsi_(scsi_bus_, CLOCK_RATE * 2), - hard_drive_(scsi_bus_, 6 /* SCSI ID */), - drives_{ - {CLOCK_RATE, model >= Analyser::Static::Macintosh::Target::Model::Mac512ke}, - {CLOCK_RATE, model >= Analyser::Static::Macintosh::Target::Model::Mac512ke} + mc68000_(*this), + iwm_(CLOCK_RATE), + video_(audio_, drive_speed_accumulator_), + via_(via_port_handler_), + via_port_handler_(*this, clock_, keyboard_, audio_, iwm_, mouse_), + scsi_bus_(CLOCK_RATE * 2), + scsi_(scsi_bus_, CLOCK_RATE * 2), + hard_drive_(scsi_bus_, 6 /* SCSI ID */), + drives_{ + {CLOCK_RATE, model >= Analyser::Static::Macintosh::Target::Model::Mac512ke}, + {CLOCK_RATE, model >= Analyser::Static::Macintosh::Target::Model::Mac512ke} }, mouse_(1) { @@ -192,14 +191,12 @@ template class ConcreteMachin mc68000_.run_for(cycles); } - using Microcycle = CPU::MC68000Mk2::Microcycle; - - HalfCycles perform_bus_operation(const Microcycle &cycle, int) { + template HalfCycles perform_bus_operation(const Microcycle &cycle, int) { // Advance time. advance_time(cycle.length); // A null cycle leaves nothing else to do. - if(!(cycle.operation & (Microcycle::NewAddress | Microcycle::SameAddress))) return HalfCycles(0); + if(!(cycle.operation & (CPU::MC68000::Operation::NewAddress | CPU::MC68000::Operation::SameAddress))) return HalfCycles(0); // Grab the address. auto address = cycle.host_endian_byte_address(); @@ -219,7 +216,10 @@ template class ConcreteMachin // having set VPA above deals with those given that the generated address // for interrupt acknowledge cycles always has all bits set except the // lowest explicit address lines. - if(!cycle.data_select_active() || (cycle.operation & Microcycle::InterruptAcknowledge)) return HalfCycles(0); + if( + !cycle.data_select_active() || + (cycle.operation & CPU::MC68000::Operation::InterruptAcknowledge) + ) return HalfCycles(0); // Grab the word-precision address being accessed. uint8_t *memory_base = nullptr; @@ -239,7 +239,7 @@ template class ConcreteMachin // VIA accesses are via address 0xefe1fe + register*512, // which at word precision is 0x77f0ff + register*256. - if(cycle.operation & Microcycle::Read) { + if(cycle.operation & CPU::MC68000::Operation::Read) { cycle.set_value8_high(via_.read(register_address)); } else { via_.write(register_address, cycle.value8_high()); @@ -248,7 +248,7 @@ template class ConcreteMachin } return delay; case BusDevice::PhaseRead: { - if(cycle.operation & Microcycle::Read) { + if(cycle.operation & CPU::MC68000::Operation::Read) { cycle.set_value8_low(phase_ & 7); } } return delay; @@ -258,7 +258,7 @@ template class ConcreteMachin const int register_address = address >> 9; // The IWM; this is a purely polled device, so can be run on demand. - if(cycle.operation & Microcycle::Read) { + if(cycle.operation & CPU::MC68000::Operation::Read) { cycle.set_value8_low(iwm_->read(register_address)); } else { iwm_->write(register_address, cycle.value8_low()); @@ -275,14 +275,14 @@ template class ConcreteMachin // Even accesses = read; odd = write. if(*cycle.address & 1) { // Odd access => this is a write. Data will be in the upper byte. - if(cycle.operation & Microcycle::Read) { + if(cycle.operation & CPU::MC68000::Operation::Read) { scsi_.write(register_address, 0xff, dma_acknowledge); } else { scsi_.write(register_address, cycle.value8_high()); } } else { // Even access => this is a read. - if(cycle.operation & Microcycle::Read) { + if(cycle.operation & CPU::MC68000::Operation::Read) { cycle.set_value8_high(scsi_.read(register_address, dma_acknowledge)); } } @@ -290,19 +290,19 @@ template class ConcreteMachin case BusDevice::SCCReadResetPhase: { // Any word access here adjusts phase. - if(cycle.operation & Microcycle::SelectWord) { + if(cycle.operation & CPU::MC68000::Operation::SelectWord) { adjust_phase(); } else { // A0 = 1 => reset; A0 = 0 => read. if(*cycle.address & 1) { scc_.reset(); - if(cycle.operation & Microcycle::Read) { + if(cycle.operation & CPU::MC68000::Operation::Read) { cycle.set_value16(0xffff); } } else { const auto read = scc_.read(int(address >> 1)); - if(cycle.operation & Microcycle::Read) { + if(cycle.operation & CPU::MC68000::Operation::Read) { cycle.set_value8_high(read); } } @@ -311,13 +311,13 @@ template class ConcreteMachin case BusDevice::SCCWrite: { // Any word access here adjusts phase. - if(cycle.operation & Microcycle::SelectWord) { + if(cycle.operation & CPU::MC68000::Operation::SelectWord) { adjust_phase(); } else { // This is definitely a byte access; either it's to an odd address, in which // case it will reach the SCC, or it isn't, in which case it won't. if(*cycle.address & 1) { - if(cycle.operation & Microcycle::Read) { + if(cycle.operation & CPU::MC68000::Operation::Read) { scc_.write(int(address >> 1), 0xff); cycle.value->b = 0xff; } else { @@ -352,7 +352,7 @@ template class ConcreteMachin } break; case BusDevice::ROM: { - if(!(cycle.operation & Microcycle::Read)) return delay; + if(!(cycle.operation & CPU::MC68000::Operation::Read)) return delay; memory_base = rom_; address &= rom_mask_; } break; @@ -544,8 +544,9 @@ template class ConcreteMachin ++phase_; } + template forceinline void fill_unmapped(const Microcycle &cycle) { - if(!(cycle.operation & Microcycle::Read)) return; + if(!(cycle.operation & CPU::MC68000::Operation::Read)) return; cycle.set_value16(0xffff); } @@ -576,7 +577,7 @@ template class ConcreteMachin video_.run_for(time_until_video_event_); time_since_video_update_ -= time_until_video_event_; - time_until_video_event_ = video_.get_next_sequence_point(); + time_until_video_event_ = video_.next_sequence_point(); via_.set_control_line_input(MOS::MOS6522::Port::A, MOS::MOS6522::Line::One, !video_.vsync()); } @@ -627,7 +628,7 @@ template class ConcreteMachin forceinline void update_video() { video_.run_for(time_since_video_update_.flush()); - time_until_video_event_ = video_.get_next_sequence_point(); + time_until_video_event_ = video_.next_sequence_point(); } Inputs::Mouse &get_mouse() final { @@ -722,7 +723,7 @@ template class ConcreteMachin if(port == Port::B && line == Line::Two) { keyboard_.set_input(value); } - else LOG("Unhandled control line output: " << (port ? 'B' : 'A') << int(line)); + else logger.error().append("Unhandled 6522 control line output: %c%d", port ? 'B' : 'A', int(line)); } void run_for(HalfCycles duration) { @@ -748,7 +749,7 @@ template class ConcreteMachin Inputs::QuadratureMouse &mouse_; }; - CPU::MC68000Mk2::Processor mc68000_; + CPU::MC68000::Processor mc68000_; DriveSpeedAccumulator drive_speed_accumulator_; IWMActor iwm_; @@ -760,20 +761,20 @@ template class ConcreteMachin Keyboard keyboard_; MOS::MOS6522::MOS6522 via_; - VIAPortHandler via_port_handler_; + VIAPortHandler via_port_handler_; - Zilog::SCC::z8530 scc_; + Zilog::SCC::z8530 scc_; SCSI::Bus scsi_bus_; - NCR::NCR5380::NCR5380 scsi_; + NCR::NCR5380::NCR5380 scsi_; SCSI::Target::Target hard_drive_; - bool scsi_bus_is_clocked_ = false; + bool scsi_bus_is_clocked_ = false; - HalfCycles via_clock_; - HalfCycles real_time_clock_; - HalfCycles keyboard_clock_; - HalfCycles time_since_video_update_; - HalfCycles time_until_video_event_; - HalfCycles time_since_mouse_update_; + HalfCycles via_clock_; + HalfCycles real_time_clock_; + HalfCycles keyboard_clock_; + HalfCycles time_since_video_update_; + HalfCycles time_until_video_event_; + HalfCycles time_since_mouse_update_; bool ROM_is_overlay_ = true; int phase_ = 1; @@ -839,16 +840,16 @@ template class ConcreteMachin using namespace Apple::Macintosh; -Machine *Machine::Macintosh(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { +std::unique_ptr Machine::Macintosh(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) { auto *const mac_target = dynamic_cast(target); using Model = Analyser::Static::Macintosh::Target::Model; switch(mac_target->model) { default: - case Model::Mac128k: return new ConcreteMachine(*mac_target, rom_fetcher); - case Model::Mac512k: return new ConcreteMachine(*mac_target, rom_fetcher); - case Model::Mac512ke: return new ConcreteMachine(*mac_target, rom_fetcher); - case Model::MacPlus: return new ConcreteMachine(*mac_target, rom_fetcher); + case Model::Mac128k: return std::make_unique>(*mac_target, rom_fetcher); + case Model::Mac512k: return std::make_unique>(*mac_target, rom_fetcher); + case Model::Mac512ke: return std::make_unique>(*mac_target, rom_fetcher); + case Model::MacPlus: return std::make_unique>(*mac_target, rom_fetcher); } } diff --git a/Machines/Apple/Macintosh/Macintosh.hpp b/Machines/Apple/Macintosh/Macintosh.hpp index 5c42b555f..d2b654690 100644 --- a/Machines/Apple/Macintosh/Macintosh.hpp +++ b/Machines/Apple/Macintosh/Macintosh.hpp @@ -6,23 +6,21 @@ // Copyright © 2019 Thomas Harte. All rights reserved. // -#ifndef Macintosh_hpp -#define Macintosh_hpp +#pragma once #include "../../../Configurable/Configurable.hpp" #include "../../../Configurable/StandardOptions.hpp" #include "../../../Analyser/Static/StaticAnalyser.hpp" #include "../../ROMMachine.hpp" -namespace Apple { -namespace Macintosh { +namespace Apple::Macintosh { class Machine { public: virtual ~Machine(); /// Creates and returns a Macintosh. - static Machine *Macintosh(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); + static std::unique_ptr Macintosh(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); class Options: public Reflection::StructImpl, public Configurable::QuickbootOption { friend Configurable::QuickbootOption; @@ -36,8 +34,4 @@ class Machine { }; }; - } -} - -#endif /* Macintosh_hpp */ diff --git a/Machines/Apple/Macintosh/Video.cpp b/Machines/Apple/Macintosh/Video.cpp index 8e1bfc156..37c7ea8e1 100644 --- a/Machines/Apple/Macintosh/Video.cpp +++ b/Machines/Apple/Macintosh/Video.cpp @@ -36,9 +36,9 @@ constexpr uint64_t PixelMask = 0x0102040810204080; Video::Video(DeferredAudio &audio, DriveSpeedAccumulator &drive_speed_accumulator) : audio_(audio), drive_speed_accumulator_(drive_speed_accumulator), - crt_(704, 1, 370, 6, Outputs::Display::InputDataType::Luminance1) { + crt_(704, 1, 370, 6, Outputs::Display::InputDataType::Luminance1) { - crt_.set_display_type(Outputs::Display::DisplayType::RGB); + crt_.set_display_type(Outputs::Display::DisplayType::RGB); // UGLY HACK. UGLY, UGLY HACK. UGLY! // The OpenGL scan target fails properly to place visible areas which are not 4:3. @@ -170,7 +170,7 @@ bool Video::vsync() { return line >= 353 && line < 356; } -HalfCycles Video::get_next_sequence_point() { +HalfCycles Video::next_sequence_point() { const auto line = (frame_position_ / line_length).as_integral(); if(line >= 353 && line < 356) { // Currently in vsync, so get time until start of line 357, diff --git a/Machines/Apple/Macintosh/Video.hpp b/Machines/Apple/Macintosh/Video.hpp index 4ade61a19..6f0ed7658 100644 --- a/Machines/Apple/Macintosh/Video.hpp +++ b/Machines/Apple/Macintosh/Video.hpp @@ -6,16 +6,14 @@ // Copyright © 2019 Thomas Harte. All rights reserved. // -#ifndef Video_hpp -#define Video_hpp +#pragma once #include "../../../Outputs/CRT/CRT.hpp" #include "../../../ClockReceiver/ClockReceiver.hpp" #include "DeferredAudio.hpp" #include "DriveSpeedAccumulator.hpp" -namespace Apple { -namespace Macintosh { +namespace Apple::Macintosh { constexpr HalfCycles line_length(704); constexpr int number_of_lines = 370; @@ -81,7 +79,7 @@ class Video { @returns the amount of time until there is next a transition on the vsync signal. */ - HalfCycles get_next_sequence_point(); + HalfCycles next_sequence_point(); private: DeferredAudio &audio_; @@ -103,6 +101,3 @@ class Video { }; } -} - -#endif /* Video_hpp */ diff --git a/Machines/Atari/2600/Atari2600.cpp b/Machines/Atari/2600/Atari2600.cpp index 72ff213d6..fb146400d 100644 --- a/Machines/Atari/2600/Atari2600.cpp +++ b/Machines/Atari/2600/Atari2600.cpp @@ -212,9 +212,9 @@ class ConcreteMachine: using namespace Atari2600; -Machine *Machine::Atari2600(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &) { +std::unique_ptr Machine::Atari2600(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &) { const Target *const atari_target = dynamic_cast(target); - return new Atari2600::ConcreteMachine(*atari_target); + return std::make_unique(*atari_target); } Machine::~Machine() {} diff --git a/Machines/Atari/2600/Atari2600.hpp b/Machines/Atari/2600/Atari2600.hpp index 43e3cddbb..4c39051a9 100644 --- a/Machines/Atari/2600/Atari2600.hpp +++ b/Machines/Atari/2600/Atari2600.hpp @@ -6,8 +6,7 @@ // Copyright 2015 Thomas Harte. All rights reserved. // -#ifndef Atari2600_cpp -#define Atari2600_cpp +#pragma once #include "../../../Configurable/Configurable.hpp" #include "../../../Analyser/Static/StaticAnalyser.hpp" @@ -15,6 +14,8 @@ #include "Atari2600Inputs.h" +#include + namespace Atari2600 { /*! @@ -25,7 +26,7 @@ class Machine { virtual ~Machine(); /// Creates and returns an Atari 2600 on the heap. - static Machine *Atari2600(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); + static std::unique_ptr Atari2600(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher); /// Sets the switch @c input to @c state. virtual void set_switch_is_enabled(Atari2600Switch input, bool state) = 0; @@ -38,5 +39,3 @@ class Machine { }; } - -#endif /* Atari2600_cpp */ diff --git a/Machines/Atari/2600/Atari2600Inputs.h b/Machines/Atari/2600/Atari2600Inputs.h index 0165f465b..b4fe86ccd 100644 --- a/Machines/Atari/2600/Atari2600Inputs.h +++ b/Machines/Atari/2600/Atari2600Inputs.h @@ -6,14 +6,13 @@ // Copyright 2015 Thomas Harte. All rights reserved. // -#ifndef Atari2600Inputs_h -#define Atari2600Inputs_h +#pragma once #ifdef __cplusplus extern "C" { #endif -typedef enum { +typedef enum { Atari2600DigitalInputJoy1Up, Atari2600DigitalInputJoy1Down, Atari2600DigitalInputJoy1Left, @@ -27,7 +26,7 @@ typedef enum { Atari2600DigitalInputJoy2Fire, } Atari2600DigitalInput; -typedef enum { +typedef enum { Atari2600SwitchReset, Atari2600SwitchSelect, Atari2600SwitchColour, @@ -38,5 +37,3 @@ typedef enum { #ifdef __cplusplus } #endif - -#endif /* Atari2600Inputs_h */ diff --git a/Machines/Atari/2600/Bus.hpp b/Machines/Atari/2600/Bus.hpp index 3fcc6f095..80e3f8354 100644 --- a/Machines/Atari/2600/Bus.hpp +++ b/Machines/Atari/2600/Bus.hpp @@ -6,8 +6,7 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef Atari2600_Bus_hpp -#define Atari2600_Bus_hpp +#pragma once #include "Atari2600.hpp" #include "PIA.hpp" @@ -67,5 +66,3 @@ class Bus { }; } - -#endif /* Atari2600_Bus_hpp */ diff --git a/Machines/Atari/2600/Cartridges/ActivisionStack.hpp b/Machines/Atari/2600/Cartridges/ActivisionStack.hpp index 025a874b4..6eca92fb8 100644 --- a/Machines/Atari/2600/Cartridges/ActivisionStack.hpp +++ b/Machines/Atari/2600/Cartridges/ActivisionStack.hpp @@ -6,11 +6,9 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef Atari2600_ActivisionStack_hpp -#define Atari2600_ActivisionStack_hpp +#pragma once -namespace Atari2600 { -namespace Cartridge { +namespace Atari2600::Cartridge { class ActivisionStack: public BusExtender { public: @@ -46,6 +44,3 @@ class ActivisionStack: public BusExtender { }; } -} - -#endif /* Atari2600_CartridgeActivisionStack_hpp */ diff --git a/Machines/Atari/2600/Cartridges/Atari16k.hpp b/Machines/Atari/2600/Cartridges/Atari16k.hpp index a98ff72fd..325e79efd 100644 --- a/Machines/Atari/2600/Cartridges/Atari16k.hpp +++ b/Machines/Atari/2600/Cartridges/Atari16k.hpp @@ -6,13 +6,11 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef Atari2600_CartridgeAtari16k_hpp -#define Atari2600_CartridgeAtari16k_hpp +#pragma once #include "Cartridge.hpp" -namespace Atari2600 { -namespace Cartridge { +namespace Atari2600::Cartridge { class Atari16k: public BusExtender { public: @@ -61,6 +59,3 @@ class Atari16kSuperChip: public BusExtender { }; } -} - -#endif /* Atari2600_CartridgeAtari16k_hpp */ diff --git a/Machines/Atari/2600/Cartridges/Atari32k.hpp b/Machines/Atari/2600/Cartridges/Atari32k.hpp index eff10e86d..e130f1a2c 100644 --- a/Machines/Atari/2600/Cartridges/Atari32k.hpp +++ b/Machines/Atari/2600/Cartridges/Atari32k.hpp @@ -6,13 +6,11 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef Atari2600_CartridgeAtari32k_hpp -#define Atari2600_CartridgeAtari32k_hpp +#pragma once #include "Cartridge.hpp" -namespace Atari2600 { -namespace Cartridge { +namespace Atari2600::Cartridge { class Atari32k: public BusExtender { public: @@ -57,6 +55,3 @@ class Atari32kSuperChip: public BusExtender { }; } -} - -#endif /* Atari2600_CartridgeAtari32k_hpp */ diff --git a/Machines/Atari/2600/Cartridges/Atari8k.hpp b/Machines/Atari/2600/Cartridges/Atari8k.hpp index 9f57527f8..03987d022 100644 --- a/Machines/Atari/2600/Cartridges/Atari8k.hpp +++ b/Machines/Atari/2600/Cartridges/Atari8k.hpp @@ -6,13 +6,11 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef Atari2600_CartridgeAtari8k_hpp -#define Atari2600_CartridgeAtari8k_hpp +#pragma once #include "Cartridge.hpp" -namespace Atari2600 { -namespace Cartridge { +namespace Atari2600::Cartridge { class Atari8k: public BusExtender { public: @@ -59,6 +57,3 @@ class Atari8kSuperChip: public BusExtender { }; } -} - -#endif /* Atari2600_CartridgeAtari8k_hpp */ diff --git a/Machines/Atari/2600/Cartridges/CBSRAMPlus.hpp b/Machines/Atari/2600/Cartridges/CBSRAMPlus.hpp index e10a5f287..3fd9586b4 100644 --- a/Machines/Atari/2600/Cartridges/CBSRAMPlus.hpp +++ b/Machines/Atari/2600/Cartridges/CBSRAMPlus.hpp @@ -6,13 +6,11 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef Atari2600_CartridgeCBSRAMPlus_hpp -#define Atari2600_CartridgeCBSRAMPlus_hpp +#pragma once #include "Cartridge.hpp" -namespace Atari2600 { -namespace Cartridge { +namespace Atari2600::Cartridge { class CBSRAMPlus: public BusExtender { public: @@ -38,6 +36,3 @@ class CBSRAMPlus: public BusExtender { }; } -} - -#endif /* Atari2600_CartridgeCBSRAMPlus_hpp */ diff --git a/Machines/Atari/2600/Cartridges/Cartridge.hpp b/Machines/Atari/2600/Cartridges/Cartridge.hpp index 90706fb04..bec59ffaf 100644 --- a/Machines/Atari/2600/Cartridges/Cartridge.hpp +++ b/Machines/Atari/2600/Cartridges/Cartridge.hpp @@ -6,14 +6,12 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef Atari2600_Cartridge_hpp -#define Atari2600_Cartridge_hpp +#pragma once #include "../../../../Processors/6502/6502.hpp" #include "../Bus.hpp" -namespace Atari2600 { -namespace Cartridge { +namespace Atari2600::Cartridge { class BusExtender: public CPU::MOS6502::BusHandler { public: @@ -215,6 +213,3 @@ template class Cartridge: }; } -} - -#endif /* Atari2600_Cartridge_hpp */ diff --git a/Machines/Atari/2600/Cartridges/CommaVid.hpp b/Machines/Atari/2600/Cartridges/CommaVid.hpp index 251abb139..0be7f5919 100644 --- a/Machines/Atari/2600/Cartridges/CommaVid.hpp +++ b/Machines/Atari/2600/Cartridges/CommaVid.hpp @@ -6,13 +6,11 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef Atari2600_CartridgeCommaVid_hpp -#define Atari2600_CartridgeCommaVid_hpp +#pragma once #include "Cartridge.hpp" -namespace Atari2600 { -namespace Cartridge { +namespace Atari2600::Cartridge { class CommaVid: public BusExtender { public: @@ -40,6 +38,3 @@ class CommaVid: public BusExtender { }; } -} - -#endif /* Atari2600_CartridgeCommaVid_hpp */ diff --git a/Machines/Atari/2600/Cartridges/MNetwork.hpp b/Machines/Atari/2600/Cartridges/MNetwork.hpp index 9ad5a0faa..06a6972c0 100644 --- a/Machines/Atari/2600/Cartridges/MNetwork.hpp +++ b/Machines/Atari/2600/Cartridges/MNetwork.hpp @@ -6,13 +6,11 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef Atari2600_CartridgeMNetwork_hpp -#define Atari2600_CartridgeMNetwork_hpp +#pragma once #include "Cartridge.hpp" -namespace Atari2600 { -namespace Cartridge { +namespace Atari2600::Cartridge { class MNetwork: public BusExtender { public: @@ -64,6 +62,3 @@ class MNetwork: public BusExtender { }; } -} - -#endif /* Atari2600_CartridgeMNetwork_hpp */ diff --git a/Machines/Atari/2600/Cartridges/MegaBoy.hpp b/Machines/Atari/2600/Cartridges/MegaBoy.hpp index a4cc2b91f..fcc021980 100644 --- a/Machines/Atari/2600/Cartridges/MegaBoy.hpp +++ b/Machines/Atari/2600/Cartridges/MegaBoy.hpp @@ -6,13 +6,11 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef Atari2600_CartridgeMegaBoy_hpp -#define Atari2600_CartridgeMegaBoy_hpp +#pragma once #include "Cartridge.hpp" -namespace Atari2600 { -namespace Cartridge { +namespace Atari2600::Cartridge { class MegaBoy: public BusExtender { public: @@ -42,6 +40,3 @@ class MegaBoy: public BusExtender { }; } -} - -#endif /* CartridgeMegaBoy_h */ diff --git a/Machines/Atari/2600/Cartridges/ParkerBros.hpp b/Machines/Atari/2600/Cartridges/ParkerBros.hpp index 559491bad..39ce52893 100644 --- a/Machines/Atari/2600/Cartridges/ParkerBros.hpp +++ b/Machines/Atari/2600/Cartridges/ParkerBros.hpp @@ -6,13 +6,11 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef Atari2600_CartridgeParkerBros_hpp -#define Atari2600_CartridgeParkerBros_hpp +#pragma once #include "Cartridge.hpp" -namespace Atari2600 { -namespace Cartridge { +namespace Atari2600::Cartridge { class ParkerBros: public BusExtender { public: @@ -43,6 +41,3 @@ class ParkerBros: public BusExtender { }; } -} - -#endif /* Atari2600_CartridgeParkerBros_hpp */ diff --git a/Machines/Atari/2600/Cartridges/Pitfall2.hpp b/Machines/Atari/2600/Cartridges/Pitfall2.hpp index 3cd2dd557..c28da6433 100644 --- a/Machines/Atari/2600/Cartridges/Pitfall2.hpp +++ b/Machines/Atari/2600/Cartridges/Pitfall2.hpp @@ -6,11 +6,9 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef Atari2600_CartridgePitfall2_hpp -#define Atari2600_CartridgePitfall2_hpp +#pragma once -namespace Atari2600 { -namespace Cartridge { +namespace Atari2600::Cartridge { class Pitfall2: public BusExtender { public: @@ -124,6 +122,3 @@ class Pitfall2: public BusExtender { }; } -} - -#endif /* Atari2600_CartridgePitfall2_hpp */ diff --git a/Machines/Atari/2600/Cartridges/Tigervision.hpp b/Machines/Atari/2600/Cartridges/Tigervision.hpp index fa2f46a51..ac21071e0 100644 --- a/Machines/Atari/2600/Cartridges/Tigervision.hpp +++ b/Machines/Atari/2600/Cartridges/Tigervision.hpp @@ -6,13 +6,11 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef Atari2600_CartridgeTigervision_hpp -#define Atari2600_CartridgeTigervision_hpp +#pragma once #include "Cartridge.hpp" -namespace Atari2600 { -namespace Cartridge { +namespace Atari2600::Cartridge { class Tigervision: public BusExtender { public: @@ -37,6 +35,3 @@ class Tigervision: public BusExtender { }; } -} - -#endif /* Atari2600_CartridgeTigervision_hpp */ diff --git a/Machines/Atari/2600/Cartridges/Unpaged.hpp b/Machines/Atari/2600/Cartridges/Unpaged.hpp index 703e127c3..f778d3f1b 100644 --- a/Machines/Atari/2600/Cartridges/Unpaged.hpp +++ b/Machines/Atari/2600/Cartridges/Unpaged.hpp @@ -6,13 +6,11 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef Atari2600_CartridgeUnpaged_hpp -#define Atari2600_CartridgeUnpaged_hpp +#pragma once #include "Cartridge.hpp" -namespace Atari2600 { -namespace Cartridge { +namespace Atari2600::Cartridge { class Unpaged: public BusExtender { public: @@ -26,6 +24,3 @@ class Unpaged: public BusExtender { }; } -} - -#endif /* Atari2600_CartridgeUnpaged_hpp */ diff --git a/Machines/Atari/2600/PIA.hpp b/Machines/Atari/2600/PIA.hpp index 61074eb0f..b25cf52d5 100644 --- a/Machines/Atari/2600/PIA.hpp +++ b/Machines/Atari/2600/PIA.hpp @@ -6,8 +6,7 @@ // Copyright 2016 Thomas Harte. All rights reserved. // -#ifndef Atari2600_PIA_h -#define Atari2600_PIA_h +#pragma once #include @@ -36,5 +35,3 @@ class PIA: public MOS::MOS6532 { }; } - -#endif /* PIA_h */ diff --git a/Machines/Atari/2600/TIA.cpp b/Machines/Atari/2600/TIA.cpp index 7eb35ce40..4f0cc761b 100644 --- a/Machines/Atari/2600/TIA.cpp +++ b/Machines/Atari/2600/TIA.cpp @@ -23,7 +23,7 @@ namespace { } TIA::TIA(): - crt_(cycles_per_line * 2 - 1, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Luminance8Phase8) { + crt_(cycles_per_line * 2 - 1, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Luminance8Phase8) { set_output_mode(OutputMode::NTSC); diff --git a/Machines/Atari/2600/TIA.hpp b/Machines/Atari/2600/TIA.hpp index a17736a4d..71ba63286 100644 --- a/Machines/Atari/2600/TIA.hpp +++ b/Machines/Atari/2600/TIA.hpp @@ -6,8 +6,7 @@ // Copyright 2017 Thomas Harte. All rights reserved. // -#ifndef TIA_hpp -#define TIA_hpp +#pragma once #include #include @@ -313,5 +312,3 @@ class TIA { }; } - -#endif /* TIA_hpp */ diff --git a/Machines/Atari/2600/TIASound.hpp b/Machines/Atari/2600/TIASound.hpp index 9688e7310..67976be58 100644 --- a/Machines/Atari/2600/TIASound.hpp +++ b/Machines/Atari/2600/TIASound.hpp @@ -6,8 +6,7 @@ // Copyright 2016 Thomas Harte. All rights reserved. // -#ifndef Atari2600_TIASound_hpp -#define Atari2600_TIASound_hpp +#pragma once #include "../../../Outputs/Speaker/Implementation/SampleSource.hpp" #include "../../../Concurrency/AsyncTaskQueue.hpp" @@ -48,5 +47,3 @@ class TIASound: public Outputs::Speaker::SampleSource { }; } - -#endif /* Speaker_hpp */ diff --git a/Machines/Atari/ST/AtariST.cpp b/Machines/Atari/ST/AtariST.cpp index 6925ad85a..bab80e959 100644 --- a/Machines/Atari/ST/AtariST.cpp +++ b/Machines/Atari/ST/AtariST.cpp @@ -13,7 +13,7 @@ //#define LOG_TRACE //bool should_log = false; -#include "../../../Processors/68000Mk2/68000Mk2.hpp" +#include "../../../Processors/68000/68000.hpp" #include "../../../Components/AY38910/AY38910.hpp" #include "../../../Components/68901/MFP68901.hpp" @@ -29,7 +29,6 @@ #include "../../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp" -#define LOG_PREFIX "[ST] " #include "../../../Outputs/Log.hpp" #include "../../Utility/MemoryPacker.hpp" @@ -37,6 +36,10 @@ #include "../../../Analyser/Static/AtariST/Target.hpp" +namespace { +Log::Logger logger; +} + namespace Atari { namespace ST { @@ -45,7 +48,7 @@ constexpr int CLOCK_RATE = 8021247; using Target = Analyser::Static::AtariST::Target; class ConcreteMachine: public Atari::ST::Machine, - public CPU::MC68000Mk2::BusHandler, + public CPU::MC68000::BusHandler, public MachineTypes::TimedMachine, public MachineTypes::ScanProducer, public MachineTypes::AudioProducer, @@ -100,8 +103,7 @@ class ConcreteMachine: Memory::PackBigEndian16(roms.find(rom_name)->second, rom_); // Set up basic memory map. - memory_map_[0] = BusDevice::MostlyRAM; - int c = 1; + int c = 0; for(; c < int(ram_.size() >> 16); ++c) memory_map_[c] = BusDevice::RAM; for(; c < 0x40; ++c) memory_map_[c] = BusDevice::Floating; for(; c < 0xff; ++c) memory_map_[c] = BusDevice::Unassigned; @@ -118,6 +120,9 @@ class ConcreteMachine: memory_map_[0xfa] = memory_map_[0xfb] = BusDevice::Cartridge; memory_map_[0xff] = BusDevice::IO; + // Copy the first 8 bytes of ROM into RAM. + reinstall_rom_vector(); + midi_acia_->set_interrupt_delegate(this); keyboard_acia_->set_interrupt_delegate(this); @@ -183,15 +188,15 @@ class ConcreteMachine: advance_time(cycle.length); // Check for assertion of reset. - if(cycle.operation & Microcycle::Reset) { - LOG("Unhandled Reset"); + if(cycle.operation & CPU::MC68000::Operation::Reset) { + logger.error().append("Unhandled Reset"); } // A null cycle leaves nothing else to do. - if(!(cycle.operation & (Microcycle::NewAddress | Microcycle::SameAddress))) return HalfCycles(0); + if(!(cycle.operation & (CPU::MC68000::Operation::NewAddress | CPU::MC68000::Operation::SameAddress))) return HalfCycles(0); // An interrupt acknowledge, perhaps? - if(cycle.operation & Microcycle::InterruptAcknowledge) { + if(cycle.operation & CPU::MC68000::Operation::InterruptAcknowledge) { // Current implementation: everything other than 6 (i.e. the MFP) is autovectored. const int interrupt_level = cycle.word_address()&7; if(interrupt_level != 6) { @@ -200,7 +205,7 @@ class ConcreteMachine: mc68000_.set_is_peripheral_address(true); return HalfCycles(0); } else { - if(cycle.operation & Microcycle::SelectByte) { + if(cycle.operation & CPU::MC68000::Operation::SelectByte) { const int interrupt = mfp_->acknowledge_interrupt(); if(interrupt != Motorola::MFP68901::MFP68901::NoAcknowledgement) { cycle.value->b = uint8_t(interrupt); @@ -217,7 +222,7 @@ class ConcreteMachine: // If this is a new strobing of the address signal, test for bus error and pre-DTack delay. HalfCycles delay(0); - if(cycle.operation & Microcycle::NewAddress) { + if(cycle.operation & CPU::MC68000::Operation::NewAddress) { // Bus error test. if( // Anything unassigned should generate a bus error. @@ -246,18 +251,15 @@ class ConcreteMachine: uint8_t *memory = nullptr; switch(memory_map_[address >> 16]) { default: - case BusDevice::MostlyRAM: - if(address < 8) { - memory = rom_.data(); - break; - } - [[fallthrough]]; case BusDevice::RAM: memory = ram_.data(); break; case BusDevice::ROM: memory = rom_.data(); + if(!(cycle.operation & CPU::MC68000::Operation::Read)) { + return delay; + } address -= rom_start_; break; @@ -269,12 +271,12 @@ class ConcreteMachine: TOS 1.0 appears to attempt to read from the catridge before it has setup the bus error vector. Therefore I assume no bus error flows. */ - switch(cycle.operation & (Microcycle::SelectWord | Microcycle::SelectByte | Microcycle::Read)) { + switch(cycle.operation & (CPU::MC68000::Operation::SelectWord | CPU::MC68000::Operation::SelectByte | CPU::MC68000::Operation::Read)) { default: break; - case Microcycle::SelectWord | Microcycle::Read: + case CPU::MC68000::Operation::SelectWord | CPU::MC68000::Operation::Read: cycle.value->w = 0xffff; break; - case Microcycle::SelectByte | Microcycle::Read: + case CPU::MC68000::Operation::SelectByte | CPU::MC68000::Operation::Read: cycle.value->b = 0xff; break; } @@ -313,7 +315,7 @@ class ConcreteMachine: case 0x8260: case 0x8262: if(!cycle.data_select_active()) return delay; - if(cycle.operation & Microcycle::Read) { + if(cycle.operation & CPU::MC68000::Operation::Read) { cycle.set_value16(video_->read(int(address >> 1))); } else { video_->write(int(address >> 1), cycle.value16()); @@ -324,7 +326,7 @@ class ConcreteMachine: case 0x8604: case 0x8606: case 0x8608: case 0x860a: case 0x860c: if(!cycle.data_select_active()) return delay; - if(cycle.operation & Microcycle::Read) { + if(cycle.operation & CPU::MC68000::Operation::Read) { cycle.set_value16(dma_->read(int(address >> 1))); } else { dma_->write(int(address >> 1), cycle.value16()); @@ -354,13 +356,12 @@ class ConcreteMachine: case 0x88d0: case 0x88d2: case 0x88d4: case 0x88d6: case 0x88d8: case 0x88da: case 0x88dc: case 0x88de: case 0x88e0: case 0x88e2: case 0x88e4: case 0x88e6: case 0x88e8: case 0x88ea: case 0x88ec: case 0x88ee: case 0x88f0: case 0x88f2: case 0x88f4: case 0x88f6: case 0x88f8: case 0x88fa: case 0x88fc: case 0x88fe: - if(!cycle.data_select_active()) return delay; advance_time(HalfCycles(2)); update_audio(); - if(cycle.operation & Microcycle::Read) { + if(cycle.operation & CPU::MC68000::Operation::Read) { cycle.set_value8_high(GI::AY38910::Utility::read(ay_)); } else { // Net effect here: addresses with bit 1 set write to a register, @@ -380,7 +381,7 @@ class ConcreteMachine: case 0xfa38: case 0xfa3a: case 0xfa3c: case 0xfa3e: if(!cycle.data_select_active()) return delay; - if(cycle.operation & Microcycle::Read) { + if(cycle.operation & CPU::MC68000::Operation::Read) { cycle.set_value8_low(mfp_->read(int(address >> 1))); } else { mfp_->write(int(address >> 1), cycle.value8_low()); @@ -394,7 +395,7 @@ class ConcreteMachine: if(!cycle.data_select_active()) return delay; const auto acia_ = (address & 4) ? &midi_acia_ : &keyboard_acia_; - if(cycle.operation & Microcycle::Read) { + if(cycle.operation & CPU::MC68000::Operation::Read) { cycle.set_value8_high((*acia_)->read(int(address >> 1))); } else { (*acia_)->write(int(address >> 1), cycle.value8_high()); @@ -405,31 +406,40 @@ class ConcreteMachine: } // If control has fallen through to here, the access is either a read from ROM, or a read or write to RAM. - switch(cycle.operation & (Microcycle::SelectWord | Microcycle::SelectByte | Microcycle::Read)) { + // + // In both write cases, immediately reinstall the first eight bytes of RAM from ROM, so that any write to + // that area is in effect a no-op. This is cheaper than the conditionality of actually checking. + switch(cycle.operation & (CPU::MC68000::Operation::SelectWord | CPU::MC68000::Operation::SelectByte | CPU::MC68000::Operation::Read)) { default: break; - case Microcycle::SelectWord | Microcycle::Read: + case CPU::MC68000::Operation::SelectWord | CPU::MC68000::Operation::Read: cycle.value->w = *reinterpret_cast(&memory[address]); break; - case Microcycle::SelectByte | Microcycle::Read: + case CPU::MC68000::Operation::SelectByte | CPU::MC68000::Operation::Read: cycle.value->b = memory[address]; break; - case Microcycle::SelectWord: + case CPU::MC68000::Operation::SelectWord: if(address >= video_range_.low_address && address < video_range_.high_address) video_.flush(); *reinterpret_cast(&memory[address]) = cycle.value->w; + reinstall_rom_vector(); break; - case Microcycle::SelectByte: + case CPU::MC68000::Operation::SelectByte: if(address >= video_range_.low_address && address < video_range_.high_address) video_.flush(); memory[address] = cycle.value->b; + reinstall_rom_vector(); break; } return HalfCycles(0); } + void reinstall_rom_vector() { + std::copy(rom_.begin(), rom_.begin() + 8, ram_.begin()); + } + void flush_output(int outputs) final { dma_.flush(); mfp_.flush(); @@ -481,7 +491,7 @@ class ConcreteMachine: length -= video_.cycles_until_implicit_flush(); video_ += video_.cycles_until_implicit_flush(); - mfp_->set_timer_event_input(1, video_->display_enabled()); + mfp_->set_timer_event_input<1>(video_->display_enabled()); update_interrupt_input(); } @@ -492,7 +502,7 @@ class ConcreteMachine: speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide_cycles(Cycles(4))); } - CPU::MC68000Mk2::Processor mc68000_; + CPU::MC68000::Processor mc68000_; HalfCycles bus_phase_; JustInTimeActor