1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-11-26 08:49:37 +00:00
CLK/Machines/Acorn/Archimedes/Archimedes.cpp
2024-05-27 14:54:09 -04:00

753 lines
23 KiB
C++

//
// Archimedes.cpp
// Clock Signal
//
// Created by Thomas Harte on 04/03/2024.
// Copyright © 2024 Thomas Harte. All rights reserved.
//
#include "Archimedes.hpp"
#include "HalfDuplexSerial.hpp"
#include "InputOutputController.hpp"
#include "Keyboard.hpp"
#include "KeyboardMapper.hpp"
#include "MemoryController.hpp"
#include "Sound.hpp"
#include "../../../Configurable/Configurable.hpp"
#include "../../AudioProducer.hpp"
#include "../../KeyboardMachine.hpp"
#include "../../MediaTarget.hpp"
#include "../../MouseMachine.hpp"
#include "../../ScanProducer.hpp"
#include "../../TimedMachine.hpp"
#include "../../../Activity/Source.hpp"
#include "../../../InstructionSets/ARM/Disassembler.hpp"
#include "../../../InstructionSets/ARM/Executor.hpp"
#include "../../../Outputs/Log.hpp"
#include "../../../Components/I2C/I2C.hpp"
#include "../../../Analyser/Static/Acorn/Target.hpp"
#include <algorithm>
#include <array>
#include <set>
#include <vector>
namespace Archimedes {
class ConcreteMachine:
public Machine,
public MachineTypes::AudioProducer,
public MachineTypes::MappedKeyboardMachine,
public MachineTypes::MediaTarget,
public MachineTypes::MouseMachine,
public MachineTypes::TimedMachine,
public MachineTypes::ScanProducer,
public Activity::Source,
public Configurable::Device
{
private:
Log::Logger<Log::Source::Archimedes> logger;
// This fictitious clock rate just means '24 MIPS, please'; it's divided elsewhere.
static constexpr int ClockRate = 24'000'000;
// Runs for 24 cycles, distributing calls to the various ticking subsystems
// 'correctly' (i.e. correctly for the approximation in use).
//
// The implementation of this is coupled to the ClockRate above, hence its
// appearance here.
template <int video_divider, bool original_speed>
void macro_tick() {
macro_counter_ -= 24;
// This is a 24-cycle window, so at 24Mhz macro_tick() is called at 1Mhz.
// Hence, required ticks are:
//
// * CPU: 24;
// * video: 24 / video_divider;
// * floppy: 8;
// * timers: 2;
// * sound: 1.
tick_cpu_video<0, video_divider, original_speed>(); tick_cpu_video<1, video_divider, original_speed>();
tick_cpu_video<2, video_divider, original_speed>(); tick_floppy();
tick_cpu_video<3, video_divider, original_speed>(); tick_cpu_video<4, video_divider, original_speed>();
tick_cpu_video<5, video_divider, original_speed>(); tick_floppy();
tick_cpu_video<6, video_divider, original_speed>(); tick_cpu_video<7, video_divider, original_speed>();
tick_cpu_video<8, video_divider, original_speed>(); tick_floppy();
tick_cpu_video<9, video_divider, original_speed>(); tick_cpu_video<10, video_divider, original_speed>();
tick_cpu_video<11, video_divider, original_speed>(); tick_floppy();
tick_timers();
tick_cpu_video<12, video_divider, original_speed>(); tick_cpu_video<13, video_divider, original_speed>();
tick_cpu_video<14, video_divider, original_speed>(); tick_floppy();
tick_cpu_video<15, video_divider, original_speed>(); tick_cpu_video<16, video_divider, original_speed>();
tick_cpu_video<17, video_divider, original_speed>(); tick_floppy();
tick_cpu_video<18, video_divider, original_speed>(); tick_cpu_video<19, video_divider, original_speed>();
tick_cpu_video<20, video_divider, original_speed>(); tick_floppy();
tick_cpu_video<21, video_divider, original_speed>(); tick_cpu_video<22, video_divider, original_speed>();
tick_cpu_video<23, video_divider, original_speed>(); tick_floppy();
tick_timers();
tick_sound();
}
int macro_counter_ = 0;
template <int offset, int video_divider, bool original_speed>
void tick_cpu_video() {
if constexpr (!(offset % video_divider)) {
tick_video();
}
// Debug mode: run CPU a lot slower. Actually at close to original advertised MIPS speed.
if constexpr (original_speed && (offset & 7)) return;
if constexpr (offset & 1) return;
tick_cpu();
}
public:
ConcreteMachine(
const Analyser::Static::Acorn::ArchimedesTarget &target,
const ROMMachine::ROMFetcher &rom_fetcher
) : executor_(*this, *this, *this) {
set_clock_rate(ClockRate);
constexpr ROM::Name risc_os = ROM::Name::AcornRISCOS311;
ROM::Request request(risc_os);
auto roms = rom_fetcher(request);
if(!request.validate(roms)) {
throw ROMMachine::Error::MissingROMs;
}
executor_.bus.set_rom(roms.find(risc_os)->second);
insert_media(target.media);
if(!target.media.disks.empty()) {
autoload_phase_ = AutoloadPhase::WaitingForStartup;
target_program_ = target.main_program;
}
fill_pipeline(0);
}
void update_interrupts() {
using Exception = InstructionSet::ARM::Registers::Exception;
const int requests = executor_.bus.interrupt_mask();
if((requests & InterruptRequests::FIQ) && executor_.registers().would_interrupt<Exception::FIQ>()) {
pipeline_.reschedule(Pipeline::SWISubversion::FIQ);
return;
}
if((requests & InterruptRequests::IRQ) && executor_.registers().would_interrupt<Exception::IRQ>()) {
pipeline_.reschedule(Pipeline::SWISubversion::IRQ);
}
}
void did_set_status() {
// This might have been a change of mode, so...
trans_ = executor_.registers().mode() == InstructionSet::ARM::Mode::User;
fill_pipeline(executor_.pc());
update_interrupts();
}
void did_set_pc() {
fill_pipeline(executor_.pc());
}
bool should_swi(uint32_t comment) {
using Exception = InstructionSet::ARM::Registers::Exception;
using SWISubversion = Pipeline::SWISubversion;
switch(pipeline_.swi_subversion()) {
case Pipeline::SWISubversion::None: {
// TODO: 400C1 to intercept create window 400C1 and positioning; then
// plot icon 400e2 to listen for icons in window. That'll give a click area.
// Probably also 400c2 which seems to be used to add icons to the icon bar.
//
// 400D4 for menus?
const auto get_string = [&](uint32_t address, bool indirect) -> std::string {
std::string desc;
if(indirect) {
executor_.bus.read(address, address, false);
}
while(true) {
uint8_t next = 0;
executor_.bus.read(address, next, false);
if(next < 0x20) break;
desc.push_back(static_cast<char>(next) & 0x7f);
++address;
}
return desc;
};
const uint32_t swi_code = comment & static_cast<uint32_t>(~(1 << 17));
switch(swi_code) {
//
// Passive monitoring traps, for automatic loading.
//
case 0x400e3: // Wimp_SetMode
case 0x65: // OS_ScreenMode
case 0x3f: // OS_CheckModeValid
if(autoload_phase_ == AutoloadPhase::OpeningProgram) {
autoload_phase_ = AutoloadPhase::Ended;
}
break;
case 0x400d4: {
if(autoload_phase_ == AutoloadPhase::TestingMenu) {
autoload_phase_ = AutoloadPhase::Ended;
uint32_t address = executor_.registers()[1] + 28;
bool should_left_click = true;
while(true) {
uint32_t icon_flags;
uint32_t item_flags;
executor_.bus.read(address, item_flags, false);
executor_.bus.read(address + 8, icon_flags, false);
auto desc = get_string(address + 12, icon_flags & (1 << 8));
should_left_click &=
(desc == "Info") ||
(desc == "Quit");
address += 24;
if(item_flags & (1 << 7)) break;
}
if(should_left_click) {
// Exit the application menu, then click once further to launch.
CursorActionBuilder(cursor_actions_)
.move_to(IconBarProgramX - 128, IconBarY - 32)
.click(0)
.move_to(IconBarProgramX, IconBarY)
.click(0);
}
}
} break;
// Wimp_OpenWindow.
case 0x400c5: {
const uint32_t address = executor_.registers()[1];
uint32_t x1, y1, x2, y2;
executor_.bus.read(address + 4, x1, false);
executor_.bus.read(address + 8, y1, false);
executor_.bus.read(address + 12, x2, false);
executor_.bus.read(address + 16, y2, false);
switch(autoload_phase_) {
default: break;
case AutoloadPhase::WaitingForDiskContents: {
autoload_phase_ = AutoloadPhase::WaitingForTargetIcon;
// Crib top left of window content.
target_window_[0] = static_cast<int32_t>(x1);
target_window_[1] = static_cast<int32_t>(y2);
} break;
case AutoloadPhase::WaitingForStartup:
if(static_cast<int32_t>(y1) == -268435472) { // VERY TEMPORARY. TODO: find better trigger.
// Creation of any icon is used to spot that RISC OS has started up.
//
// Wait a further second, mouse down to (32, 240), left click.
// That'll trigger disk access. Then move up to the top left,
// in anticipation of the appearance of a window.
auto &builder = CursorActionBuilder(cursor_actions_)
.move_to(IconBarDriveX, IconBarY)
.click(0);
if(target_program_.empty()) {
autoload_phase_ = AutoloadPhase::Ended;
} else {
autoload_phase_ = AutoloadPhase::WaitingForDiskContents;
builder.move_to(IconBarDriveX, 80); // Just a guess of 'close' to where the program to launch
// will probably be, to have the cursor broadly nearby.
}
}
break;
}
} break;
// Wimp_CreateIcon, which also adds to the icon bar.
case 0x400c2:
switch(autoload_phase_) {
case AutoloadPhase::OpeningProgram: {
const uint32_t address = executor_.registers()[1];
uint32_t handle;
executor_.bus.read(address, handle, false);
// Test whether the program has added an icon on the right.
if(static_cast<int32_t>(handle) == -1) {
CursorActionBuilder(cursor_actions_)
.move_to(IconBarProgramX, IconBarY)
.click(1);
autoload_phase_ = AutoloadPhase::TestingMenu;
}
} break;
default: break;
}
break;
// Wimp_PlotIcon.
case 0x400e2: {
if(autoload_phase_ == AutoloadPhase::WaitingForTargetIcon) {
const uint32_t address = executor_.registers()[1];
uint32_t flags;
executor_.bus.read(address + 16, flags, false);
std::string desc;
if(flags & 1) {
desc = get_string(address + 20, flags & (1 << 8));
}
if(desc == target_program_) {
uint32_t x1, y1, x2, y2;
executor_.bus.read(address + 0, x1, false);
executor_.bus.read(address + 4, y1, false);
executor_.bus.read(address + 8, x2, false);
executor_.bus.read(address + 12, y2, false);
autoload_phase_ = AutoloadPhase::OpeningProgram;
// Some default icon sizing assumptions are baked in here.
const auto x_target = target_window_[0] + (static_cast<int32_t>(x1) + static_cast<int32_t>(x2)) / 2;
const auto y_target = target_window_[1] + static_cast<int32_t>(y1) + 24;
CursorActionBuilder(cursor_actions_)
.move_to(x_target >> 1, 256 - (y_target >> 2))
.double_click(0);
}
}
} break;
}
} return true;
case SWISubversion::DataAbort:
// executor_.set_pc(executor_.pc() - 4);
executor_.registers().exception<Exception::DataAbort>();
break;
// FIQ and IRQ decrement the PC because their apperance in the pipeline causes
// it to look as though they were fetched, but they weren't.
case SWISubversion::FIQ:
executor_.set_pc(executor_.pc() - 4);
executor_.registers().exception<Exception::FIQ>();
break;
case SWISubversion::IRQ:
executor_.set_pc(executor_.pc() - 4);
executor_.registers().exception<Exception::IRQ>();
break;
}
did_set_pc();
return false;
}
void update_clock_rates() {
video_divider_ = executor_.bus.video().clock_divider();
}
private:
// MARK: - ScanProducer.
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
executor_.bus.video().crt().set_scan_target(scan_target);
}
Outputs::Display::ScanStatus get_scaled_scan_status() const override {
return executor_.bus.video().crt().get_scaled_scan_status() * video_divider_;
}
// MARK: - TimedMachine.
bool use_original_speed() const {
#ifndef NDEBUG
// Debug mode: always run 'slowly' because that's less of a burden, and
// because it allows me to peer at problems with greater leisure.
return true;
#else
// As a first, blunt implementation: try to model something close
// to original speed if there have been 10 frame rate overages in total.
return executor_.bus.video().frame_rate_overages() > 10;
#endif
}
int video_divider_ = 1;
void run_for(Cycles cycles) override {
const auto run = [&](Cycles cycles) {
if(use_original_speed()) run_for<true>(cycles);
else run_for<false>(cycles);
};
//
// Short-circuit: no cursor actions means **just run**.
//
if(cursor_actions_.empty()) {
run(cycles);
return;
}
//
// Mouse scripting; tick at a minimum of frame length.
//
static constexpr int TickFrequency = 24'000'000 / 50;
cursor_action_subcycle_ += cycles;
auto segments = cursor_action_subcycle_.divide(Cycles(TickFrequency)).as<int>();
while(segments--) {
Cycles next = Cycles(TickFrequency);
if(next > cycles) next = cycles;
cycles -= next;
if(!cursor_actions_.empty()) {
const auto move_to_next = [&]() {
cursor_action_waited_ = 0;
cursor_actions_.erase(cursor_actions_.begin());
};
const auto &action = cursor_actions_.front();
switch(action.type) {
case CursorAction::Type::MoveTo: {
// A measure of where within the tip lies within
// the default RISC OS cursor.
constexpr int ActionPointOffset = 20;
constexpr int MaxStep = 24;
const auto position = executor_.bus.video().cursor_location();
if(!position) break;
const auto [x, y] = *position;
auto x_diff = action.value.move_to.x - (x + ActionPointOffset);
auto y_diff = action.value.move_to.y - y;
if(abs(x_diff) < 2 && abs(y_diff) < 2) {
move_to_next();
break;
}
if(abs(y_diff) > MaxStep || abs(x_diff) > MaxStep) {
if(abs(y_diff) > abs(x_diff)) {
x_diff = (x_diff * MaxStep + (abs(y_diff) >> 1)) / abs(y_diff);
y_diff = std::clamp(y_diff, -MaxStep, MaxStep);
} else {
y_diff = (y_diff * MaxStep + (abs(x_diff) >> 1)) / abs(x_diff);
x_diff = std::clamp(x_diff, -MaxStep, MaxStep);
}
}
get_mouse().move(x_diff, y_diff);
} break;
case CursorAction::Type::Wait:
cursor_action_waited_ += next.as<int>();
if(cursor_action_waited_ >= action.value.wait.duration) {
move_to_next();
}
break;
case CursorAction::Type::Button:
get_mouse().set_button_pressed(action.value.button.button, action.value.button.down);
move_to_next();
break;
case CursorAction::Type::SetPhase:
autoload_phase_ = action.value.set_phase.phase;
move_to_next();
break;
}
}
//
// Execution proper.
//
run(next);
}
}
template <bool original_speed>
void run_for(Cycles cycles) {
macro_counter_ += cycles.as<int>();
while(macro_counter_ > 0) {
switch(video_divider_) {
default: macro_tick<2, original_speed>(); break;
case 3: macro_tick<3, original_speed>(); break;
case 4: macro_tick<4, original_speed>(); break;
case 6: macro_tick<6, original_speed>(); break;
}
}
}
void tick_cpu() {
const uint32_t instruction = advance_pipeline(executor_.pc() + 8);
InstructionSet::ARM::execute(instruction, executor_);
}
void tick_timers() { executor_.bus.tick_timers(); }
void tick_sound() { executor_.bus.sound().tick(); }
void tick_video() { executor_.bus.video().tick(); }
bool accelerate_loading_ = true;
void tick_floppy() {
executor_.bus.tick_floppy(
accelerate_loading_ ? (use_original_speed() ? 12 : 18) : 1
);
}
// MARK: - MediaTarget
bool insert_media(const Analyser::Static::Media &media) override {
size_t c = 0;
for(auto &disk : media.disks) {
executor_.bus.set_disk(disk, c);
c++;
if(c == 4) break;
}
return true;
}
// MARK: - Configuration options.
std::unique_ptr<Reflection::Struct> get_options() final {
auto options = std::make_unique<Options>(Configurable::OptionsType::UserFriendly);
options->quickload = accelerate_loading_;
return options;
}
void set_options(const std::unique_ptr<Reflection::Struct> &str) final {
const auto options = dynamic_cast<Options *>(str.get());
accelerate_loading_ = options->quickload;
}
// MARK: - AudioProducer
Outputs::Speaker::Speaker *get_speaker() override {
return executor_.bus.speaker();
}
// MARK: - Activity::Source.
void set_activity_observer(Activity::Observer *observer) final {
executor_.bus.set_activity_observer(observer);
}
// MARK: - MappedKeyboardMachine.
MappedKeyboardMachine::KeyboardMapper *get_keyboard_mapper() override {
return &keyboard_mapper_;
}
Archimedes::KeyboardMapper keyboard_mapper_;
void set_key_state(uint16_t key, bool is_pressed) override {
executor_.bus.keyboard().set_key_state(key, is_pressed);
}
// MARK: - MouseMachine.
Inputs::Mouse &get_mouse() override {
return executor_.bus.keyboard().mouse();
}
// MARK: - ARM execution.
static constexpr auto arm_model = InstructionSet::ARM::Model::ARMv2;
using Executor = InstructionSet::ARM::Executor<arm_model, MemoryController<ConcreteMachine, ConcreteMachine>, ConcreteMachine>;
Executor executor_;
bool trans_ = false;
void fill_pipeline(uint32_t pc) {
if(pipeline_.interrupt_next()) return;
advance_pipeline(pc);
advance_pipeline(pc + 4);
}
uint32_t advance_pipeline(uint32_t pc) {
uint32_t instruction = 0; // Value should never be used; this avoids a spurious GCC warning.
const bool did_read = executor_.bus.read(pc, instruction, trans_);
return pipeline_.exchange(
did_read ? instruction : Pipeline::SWI,
did_read ? Pipeline::SWISubversion::None : Pipeline::SWISubversion::DataAbort);
}
struct Pipeline {
enum SWISubversion: uint8_t {
None,
DataAbort,
IRQ,
FIQ,
};
static constexpr uint32_t SWI = 0xef'000000;
uint32_t exchange(uint32_t next, SWISubversion subversion) {
const uint32_t result = upcoming_[active_].opcode;
latched_subversion_ = upcoming_[active_].subversion;
upcoming_[active_].opcode = next;
upcoming_[active_].subversion = subversion;
active_ ^= 1;
return result;
}
SWISubversion swi_subversion() const {
return latched_subversion_;
}
// TODO: one day, possibly: schedule the subversion one slot further into the future
// (i.e. active_ ^ 1) to allow one further instruction to occur as usual before the
// action paplies. That is, if interrupts take effect one instruction later after a flags
// change, which I don't yet know.
//
// In practice I got into a bit of a race condition between interrupt scheduling and
// flags changes, so have backed off for now.
void reschedule(SWISubversion subversion) {
upcoming_[active_].opcode = SWI;
upcoming_[active_].subversion = subversion;
}
bool interrupt_next() const {
return upcoming_[active_].subversion == SWISubversion::IRQ || upcoming_[active_].subversion == SWISubversion::FIQ;
}
private:
struct Stage {
uint32_t opcode;
SWISubversion subversion = SWISubversion::None;
};
Stage upcoming_[2];
int active_ = 0;
SWISubversion latched_subversion_;
} pipeline_;
// MARK: - Autoload, including cursor scripting.
enum class AutoloadPhase {
WaitingForStartup,
WaitingForDiskContents,
WaitingForTargetIcon,
OpeningProgram,
TestingMenu,
Ended,
};
AutoloadPhase autoload_phase_ = AutoloadPhase::Ended;
std::string target_program_;
struct CursorAction {
enum class Type {
MoveTo,
Button,
Wait,
SetPhase,
} type;
union {
struct {
int x, y;
} move_to;
struct {
int duration;
} wait;
struct {
int button;
bool down;
} button;
struct {
AutoloadPhase phase;
} set_phase;
} value;
static CursorAction move_to(int x, int y) {
CursorAction action;
action.type = Type::MoveTo;
action.value.move_to.x = x;
action.value.move_to.y = y;
return action;
}
static CursorAction wait(int duration) {
CursorAction action;
action.type = Type::Wait;
action.value.wait.duration = duration;
return action;
}
static CursorAction button(int button, bool down) {
CursorAction action;
action.type = Type::Button;
action.value.button.button = button;
action.value.button.down = down;
return action;
}
static CursorAction set_phase(AutoloadPhase phase) {
CursorAction action;
action.type = Type::SetPhase;
action.value.set_phase.phase = phase;
return action;
}
};
std::vector<CursorAction> cursor_actions_;
struct CursorActionBuilder {
CursorActionBuilder(std::vector<CursorAction> &actions) : actions_(actions) {}
CursorActionBuilder &wait(int duration) {
actions_.push_back(CursorAction::wait(duration));
return *this;
}
CursorActionBuilder &move_to(int x, int y) {
// Special case: if this sets a move_to when one is in progress,
// just update the target.
if(!actions_.empty() && actions_.back().type == CursorAction::Type::MoveTo) {
actions_.back().value.move_to.x = x;
actions_.back().value.move_to.y = y;
return *this;
}
actions_.push_back(CursorAction::move_to(x, y));
return *this;
}
CursorActionBuilder &click(int button) {
actions_.push_back(CursorAction::button(button, true));
actions_.push_back(CursorAction::wait(6'000'000));
actions_.push_back(CursorAction::button(button, false));
return *this;
}
CursorActionBuilder &double_click(int button) {
actions_.push_back(CursorAction::button(button, true));
actions_.push_back(CursorAction::wait(6'000'000));
actions_.push_back(CursorAction::button(button, false));
actions_.push_back(CursorAction::wait(6'000'000));
actions_.push_back(CursorAction::button(button, true));
actions_.push_back(CursorAction::wait(6'000'000));
actions_.push_back(CursorAction::button(button, false));
return *this;
}
CursorActionBuilder &set_phase(AutoloadPhase phase) {
actions_.push_back(CursorAction::set_phase(phase));
return *this;
}
std::vector<CursorAction> &actions_;
};
static constexpr int IconBarY = 240;
static constexpr int IconBarProgramX = 532;
static constexpr int IconBarDriveX = 32;
std::vector<CursorAction> &begin();
Cycles cursor_action_subcycle_;
int cursor_action_waited_;
int32_t target_window_[2];
};
}
using namespace Archimedes;
std::unique_ptr<Machine> Machine::Archimedes(
const Analyser::Static::Target *target,
const ROMMachine::ROMFetcher &rom_fetcher
) {
const auto archimedes_target = dynamic_cast<const Analyser::Static::Acorn::ArchimedesTarget *>(target);
return std::make_unique<ConcreteMachine>(*archimedes_target, rom_fetcher);
}