1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-12-27 01:31:42 +00:00

Merge branch 'master' into AYFidelity

This commit is contained in:
Thomas Harte 2017-08-11 14:41:08 -04:00
commit e4f04d0977
30 changed files with 784 additions and 190 deletions

View File

@ -31,9 +31,18 @@ class BusHandler {
void perform_bus_cycle(const BusState &) {}
};
enum Personality {
HD6845S, //
UM6845R, //
MC6845, //
AMS40226 //
};
template <class T> class CRTC6845 {
public:
CRTC6845(T &bus_handler) : bus_handler_(bus_handler) {}
CRTC6845(Personality p, T &bus_handler) :
personality_(p), bus_handler_(bus_handler) {}
void run_for(Cycles cycles) {
int cyles_remaining = cycles.as_int();
@ -58,22 +67,18 @@ template <class T> class CRTC6845 {
if(hsync_down_counter_) bus_state_.hsync = true;
}
// check for end of visible characters
if(character_counter_ == registers_[1]) {
bus_state_.refresh_address++;
character_is_visible_ = false;
} else {
// update refresh address
if(character_is_visible_) {
bus_state_.refresh_address++;
}
// check for end of visible characters
if(character_counter_ == registers_[1]) {
character_is_visible_ = false;
}
// check for end-of-line
if(is_end_of_line) {
character_counter_ = 0;
character_is_visible_ = true;
// check for end of vertical sync
if(vsync_down_counter_) {
vsync_down_counter_--;
@ -94,11 +99,12 @@ template <class T> class CRTC6845 {
} else {
// advance vertical counter
if(bus_state_.row_address == registers_[9]) {
if(!character_is_visible_)
line_address_ = bus_state_.refresh_address;
bus_state_.row_address = 0;
bool is_at_end_of_frame = line_counter_ == registers_[4];
line_counter_++;
line_counter_ = (line_counter_ + 1) & 0x7f;
// check for end of visible lines
if(line_counter_ == registers_[6]) {
@ -125,19 +131,23 @@ template <class T> class CRTC6845 {
line_counter_ = 0;
}
} else {
bus_state_.row_address++;
bus_state_.row_address = (bus_state_.row_address + 1) & 0x1f;
}
bus_state_.refresh_address = line_address_;
}
}
character_counter_ = 0;
character_is_visible_ = true;
}
bus_state_.display_enable = character_is_visible_ && line_is_visible_;
bus_state_.refresh_address &= 0x3fff;
bus_handler_.perform_bus_cycle(bus_state_);
}
}
void select_register(uint8_t r) {
selected_register_ = (int)r & 15;
selected_register_ = r;
}
uint8_t get_status() {
@ -145,18 +155,31 @@ template <class T> class CRTC6845 {
}
uint8_t get_register() {
if(selected_register_ < 12 || selected_register_ > 17) return 0xff;
return registers_[selected_register_];
}
void set_register(uint8_t value) {
registers_[selected_register_] = value;
static uint8_t masks[] = {
0xff, 0xff, 0xff, 0xff, 0x7f, 0x1f, 0x7f, 0x7f,
0xff, 0x1f, 0x7f, 0x1f, 0x3f, 0xff, 0x3f, 0xff
};
if(selected_register_ < 16)
registers_[selected_register_] = value & masks[selected_register_];
}
void trigger_light_pen() {
registers_[17] = bus_state_.refresh_address & 0xff;
registers_[16] = bus_state_.refresh_address >> 8;
}
private:
Personality personality_;
T &bus_handler_;
BusState bus_state_;
uint8_t registers_[16];
uint8_t registers_[18];
int selected_register_;
uint8_t character_counter_;

View File

@ -13,10 +13,10 @@
using namespace Intel;
namespace {
const uint8_t StatusRQM = 0x80; // Set: ready to send or receive from processor.
const uint8_t StatusDIO = 0x40; // Set: data is expected to be taken from the 8272 by the processor.
const uint8_t StatusNDM = 0x20; // Set: the execution phase of a data transfer command is ongoing and DMA mode is disabled.
const uint8_t StatusCB = 0x10; // Set: the FDC is busy.
const uint8_t StatusRequest = 0x80; // Set: ready to send or receive from processor.
const uint8_t StatusDirection = 0x40; // Set: data is expected to be taken from the 8272 by the processor.
const uint8_t StatusNonDMAExecuting = 0x20; // Set: the execution phase of a data transfer command is ongoing and DMA mode is disabled.
const uint8_t StatusBusy = 0x10; // Set: the FDC is busy.
//const uint8_t StatusD3B = 0x08; // Set: drive 3 is seeking.
//const uint8_t StatusD2B = 0x04; // Set: drive 2 is seeking.
//const uint8_t StatusD1B = 0x02; // Set: drive 1 is seeking.
@ -25,7 +25,7 @@ const uint8_t StatusCB = 0x10; // Set: the FDC is busy.
i8272::i8272(Cycles clock_rate, int clock_rate_multiplier, int revolutions_per_minute) :
Storage::Disk::MFMController(clock_rate, clock_rate_multiplier, revolutions_per_minute),
main_status_(StatusRQM),
main_status_(0),
interesting_event_mask_((int)Event8272::CommandByte),
resume_point_(0),
delay_time_(0),
@ -76,7 +76,7 @@ void i8272::set_register(int address, uint8_t value) {
if(!address) return;
// if not ready for commands, do nothing
if(!(main_status_ & StatusRQM)) return;
if(!(main_status_ & StatusRequest)) return;
// accumulate latest byte in the command byte sequence
command_.push_back(value);
@ -135,7 +135,7 @@ void i8272::set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int drive) {
set_data_mode(Scanning);
#define SET_DRIVE_HEAD_MFM() \
if(!dma_mode_) main_status_ |= StatusNDM; \
if(!dma_mode_) main_status_ |= StatusNonDMAExecuting; \
set_drive(drives_[command_[1]&3].drive); \
set_is_double_density(command_[0] & 0x40); \
invalidate_track();
@ -150,94 +150,94 @@ void i8272::posit_event(int event_type) {
// into wait_for_complete_command_sequence.
wait_for_command:
set_data_mode(Storage::Disk::MFMController::DataMode::Scanning);
main_status_ &= ~(StatusCB | StatusNDM);
main_status_ &= ~(StatusBusy | StatusNonDMAExecuting);
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:
main_status_ |= StatusRQM;
main_status_ |= StatusRequest;
WAIT_FOR_EVENT(Event8272::CommandByte)
main_status_ |= StatusCB;
main_status_ |= StatusBusy;
switch(command_[0] & 0x1f) {
case 0x06: // read data
if(command_.size() < 9) goto wait_for_complete_command_sequence;
main_status_ &= ~StatusRQM;
main_status_ &= ~StatusRequest;
goto read_data;
case 0x0b: // read deleted data
if(command_.size() < 9) goto wait_for_complete_command_sequence;
main_status_ &= ~StatusRQM;
main_status_ &= ~StatusRequest;
goto read_deleted_data;
case 0x05: // write data
if(command_.size() < 9) goto wait_for_complete_command_sequence;
main_status_ &= ~StatusRQM;
main_status_ &= ~StatusRequest;
goto write_data;
case 0x09: // write deleted data
if(command_.size() < 9) goto wait_for_complete_command_sequence;
main_status_ &= ~StatusRQM;
main_status_ &= ~StatusRequest;
goto write_deleted_data;
case 0x02: // read track
if(command_.size() < 9) goto wait_for_complete_command_sequence;
main_status_ &= ~StatusRQM;
main_status_ &= ~StatusRequest;
goto read_track;
case 0x0a: // read ID
if(command_.size() < 2) goto wait_for_complete_command_sequence;
main_status_ &= ~StatusRQM;
main_status_ &= ~StatusRequest;
goto read_id;
case 0x0d: // format track
if(command_.size() < 6) goto wait_for_complete_command_sequence;
main_status_ &= ~StatusRQM;
main_status_ &= ~StatusRequest;
goto format_track;
case 0x11: // scan low
if(command_.size() < 9) goto wait_for_complete_command_sequence;
main_status_ &= ~StatusRQM;
main_status_ &= ~StatusRequest;
goto scan_low;
case 0x19: // scan low or equal
if(command_.size() < 9) goto wait_for_complete_command_sequence;
main_status_ &= ~StatusRQM;
main_status_ &= ~StatusRequest;
goto scan_low_or_equal;
case 0x1d: // scan high or equal
if(command_.size() < 9) goto wait_for_complete_command_sequence;
main_status_ &= ~StatusRQM;
main_status_ &= ~StatusRequest;
goto scan_high_or_equal;
case 0x07: // recalibrate
if(command_.size() < 2) goto wait_for_complete_command_sequence;
main_status_ &= ~StatusRQM;
main_status_ &= ~StatusRequest;
goto recalibrate;
case 0x08: // sense interrupt status
main_status_ &= ~StatusRQM;
main_status_ &= ~StatusRequest;
goto sense_interrupt_status;
case 0x03: // specify
if(command_.size() < 3) goto wait_for_complete_command_sequence;
main_status_ &= ~StatusRQM;
main_status_ &= ~StatusRequest;
goto specify;
case 0x04: // sense drive status
if(command_.size() < 2) goto wait_for_complete_command_sequence;
main_status_ &= ~StatusRQM;
main_status_ &= ~StatusRequest;
goto sense_drive_status;
case 0x0f: // seek
if(command_.size() < 3) goto wait_for_complete_command_sequence;
main_status_ &= ~StatusRQM;
main_status_ &= ~StatusRequest;
goto seek;
default: // invalid
main_status_ &= ~StatusRQM;
main_status_ &= ~StatusRequest;
goto invalid;
}
@ -253,6 +253,8 @@ void i8272::posit_event(int event_type) {
sector_ = command_[4];
size_ = command_[5];
read_next_data:
// Sets a maximum index hole limit of 2 then performs a find header/read header loop, continuing either until
// 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.
@ -277,15 +279,21 @@ void i8272::posit_event(int event_type) {
WAIT_FOR_EVENT(Event::Token);
result_stack_.push_back(get_latest_token().byte_value);
distance_into_section_++;
main_status_ |= StatusRQM | StatusDIO;
main_status_ |= StatusRequest | StatusDirection;
WAIT_FOR_EVENT(Event8272::ResultEmpty);
main_status_ &= ~StatusRQM;
main_status_ &= ~StatusRequest;
if(distance_into_section_ < (128 << size_)) goto get_byte;
// read CRC, without transferring it
WAIT_FOR_EVENT(Event::Token);
WAIT_FOR_EVENT(Event::Token);
// check whether that's it
if(sector_ != command_[6]) {
sector_++;
goto read_next_data;
}
// For a final result phase, post the standard ST0, ST1, ST2, C, H, R, N
goto post_st012chrn;
@ -416,8 +424,17 @@ void i8272::posit_event(int event_type) {
goto wait_for_command;
sense_drive_status:
printf("Sense drive status unimplemented!!\n");
goto wait_for_command;
{
int drive = command_[1] & 3;
result_stack_.push_back(
(command_[1] & 7) | // drive and head number
0x08 | // single sided
(drives_[drive].drive->get_is_track_zero() ? 0x10 : 0x00) |
(drives_[drive].drive->has_disk() ? 0x20 : 0x00) | // ready, approximately (TODO)
0x40 // write protected
);
}
goto post_result;
// Performs any invalid command.
invalid:
@ -442,15 +459,15 @@ void i8272::posit_event(int event_type) {
// last thing in it will be returned first.
post_result:
// Set ready to send data to the processor, no longer in non-DMA execution phase.
main_status_ |= StatusRQM | StatusDIO;
main_status_ &= ~StatusNDM;
main_status_ |= StatusRequest | StatusDirection;
main_status_ &= ~StatusNonDMAExecuting;
// The actual stuff of unwinding result_stack_ is handled by ::get_register; wait
// until the processor has read all result bytes.
WAIT_FOR_EVENT(Event8272::ResultEmpty);
// Reset data direction and end the command.
main_status_ &= ~StatusDIO;
main_status_ &= ~StatusDirection;
goto wait_for_command;
END_SECTION()

View File

@ -8,6 +8,8 @@
#include "AmstradCPC.hpp"
#include "CharacterMapper.hpp"
#include "../../Processors/Z80/Z80.hpp"
#include "../../Components/6845/CRTC6845.hpp"
@ -15,6 +17,9 @@
#include "../../Components/8272/i8272.hpp"
#include "../../Components/AY38910/AY38910.hpp"
#include "../MemoryFuzzer.hpp"
#include "../Typer.hpp"
#include "../../Storage/Tape/Tape.hpp"
namespace AmstradCPC {
@ -291,6 +296,7 @@ class CRTCBusHandler {
"return vec3(float((sample >> 4) & 3u), float((sample >> 2) & 3u), float(sample & 3u)) / 2.0;"
"}");
crt_->set_visible_area(Outputs::CRT::Rect(0.075f, 0.05f, 0.9f, 0.9f));
crt_->set_output_device(Outputs::CRT::Monitor);
}
/// Destructs the CRT.
@ -522,19 +528,23 @@ class i8255PortHandler : public Intel::i8255::PortHandler {
The actual Amstrad CPC implementation; tying the 8255, 6845 and AY to the Z80.
*/
class ConcreteMachine:
public Utility::TypeRecipient,
public CPU::Z80::BusHandler,
public Machine {
public:
ConcreteMachine() :
z80_(*this),
crtc_counter_(HalfCycles(4)), // This starts the CRTC exactly out of phase with the memory accesses
crtc_(crtc_bus_handler_),
crtc_(Motorola::CRTC::HD6845S, crtc_bus_handler_),
crtc_bus_handler_(ram_, interrupt_timer_),
i8255_(i8255_port_handler_),
i8255_port_handler_(key_state_, crtc_bus_handler_, ay_, tape_player_),
tape_player_(8000000) {
// primary clock is 4Mhz
set_clock_rate(4000000);
// ensure memory starts in a random state
Memory::Fuzz(ram_, sizeof(ram_));
}
/// The entry point for performing a partial Z80 machine cycle.
@ -556,12 +566,15 @@ class ConcreteMachine:
// run_for as HalfCycles
tape_player_.run_for(cycle.length.as_int());
// Pump the AY.
// Pump the AY
ay_.run_for(cycle.length);
// Clock the FDC, if connected, using a lazy scale by two
if(has_fdc_) fdc_.run_for(Cycles(cycle.length.as_int()));
// Update typing activity
if(typer_) typer_->run_for(cycle.length);
// Stop now if no action is strictly required.
if(!cycle.is_terminal()) return HalfCycles(0);
@ -579,48 +592,7 @@ class ConcreteMachine:
case CPU::Z80::PartialMachineCycle::Output:
// Check for a gate array access.
if((address & 0xc000) == 0x4000) {
switch(*cycle.value >> 6) {
case 0: crtc_bus_handler_.select_pen(*cycle.value & 0x1f); break;
case 1: crtc_bus_handler_.set_colour(*cycle.value & 0x1f); break;
case 2:
// Perform ROM paging.
read_pointers_[0] = (*cycle.value & 4) ? write_pointers_[0] : roms_[rom_model_].data();
upper_rom_is_paged_ = !(*cycle.value & 8);
read_pointers_[3] = upper_rom_is_paged_ ? roms_[upper_rom_].data() : write_pointers_[3];
// Reset the interrupt timer if requested.
if(*cycle.value & 0x10) interrupt_timer_.reset_count();
// Post the next mode.
crtc_bus_handler_.set_next_mode(*cycle.value & 3);
break;
case 3:
// Perform RAM paging, if 128kb is permitted.
if(has_128k_) {
bool adjust_low_read_pointer = read_pointers_[0] == write_pointers_[0];
bool adjust_high_read_pointer = read_pointers_[3] == write_pointers_[3];
#define RAM_BANK(x) &ram_[x * 16384]
#define RAM_CONFIG(a, b, c, d) write_pointers_[0] = RAM_BANK(a); write_pointers_[1] = RAM_BANK(b); write_pointers_[2] = RAM_BANK(c); write_pointers_[3] = RAM_BANK(d);
switch(*cycle.value & 7) {
case 0: RAM_CONFIG(0, 1, 2, 3); break;
case 1: RAM_CONFIG(0, 1, 2, 7); break;
case 2: RAM_CONFIG(4, 5, 6, 7); break;
case 3: RAM_CONFIG(0, 3, 2, 7); break;
case 4: RAM_CONFIG(0, 4, 2, 3); break;
case 5: RAM_CONFIG(0, 5, 2, 3); break;
case 6: RAM_CONFIG(0, 6, 2, 3); break;
case 7: RAM_CONFIG(0, 7, 2, 3); break;
}
#undef RAM_CONFIG
#undef RAM_BANK
if(adjust_low_read_pointer) read_pointers_[0] = write_pointers_[0];
read_pointers_[1] = write_pointers_[1];
read_pointers_[2] = write_pointers_[2];
if(adjust_high_read_pointer) read_pointers_[3] = write_pointers_[3];
}
break;
}
write_to_gate_array(*cycle.value);
}
// Check for an upper ROM selection
@ -634,7 +606,7 @@ class ConcreteMachine:
switch((address >> 8) & 3) {
case 0: crtc_.select_register(*cycle.value); break;
case 1: crtc_.set_register(*cycle.value); break;
case 2: case 3: printf("Illegal CRTC write?\n"); break;
default: break;
}
}
@ -657,23 +629,31 @@ class ConcreteMachine:
// Default to nothing answering
*cycle.value = 0xff;
// Check for a CRTC access
if(!(address & 0x4000)) {
switch((address >> 8) & 3) {
case 0: case 1: printf("Illegal CRTC read?\n"); break;
case 2: *cycle.value = crtc_.get_status(); break;
case 3: *cycle.value = crtc_.get_register(); break;
}
}
// Check for a PIO access
if(!(address & 0x800)) {
*cycle.value = i8255_.get_register((address >> 8) & 3);
*cycle.value &= i8255_.get_register((address >> 8) & 3);
}
// Check for an FDC access
if(has_fdc_ && (address & 0x580) == 0x100) {
*cycle.value = fdc_.get_register(address & 1);
*cycle.value &= fdc_.get_register(address & 1);
}
// Check for a CRTC access; the below is not a typo — the CRTC can be selected
// for writing via an input, and will sample whatever happens to be available
if(!(address & 0x4000)) {
switch((address >> 8) & 3) {
case 0: crtc_.select_register(*cycle.value); break;
case 1: crtc_.set_register(*cycle.value); break;
case 2: *cycle.value &= crtc_.get_status(); break;
case 3: *cycle.value &= crtc_.get_register(); break;
}
}
// As with the CRTC, the gate array will sample the bus if the address decoding
// implies that it should, unaware of data direction
if((address & 0xc000) == 0x4000) {
write_to_gate_array(*cycle.value);
}
break;
@ -774,6 +754,11 @@ class ConcreteMachine:
c++;
if(c == 4) break;
}
// Type whatever is required.
if(target.loadingCommand.length()) {
set_typer_for_string(target.loadingCommand.c_str());
}
}
// See header; provides the system ROMs.
@ -781,6 +766,21 @@ class ConcreteMachine:
roms_[(int)type] = data;
}
#pragma mark - Keyboard
void set_typer_for_string(const char *string) {
std::unique_ptr<CharacterMapper> mapper(new CharacterMapper());
Utility::TypeRecipient::set_typer_for_string(string, std::move(mapper));
}
HalfCycles get_typer_delay() {
return Cycles(4000000); // Wait 1 second before typing.
}
HalfCycles get_typer_frequency() {
return Cycles(80000); // Type one character per frame.
}
// See header; sets a key as either pressed or released.
void set_key_state(uint16_t key, bool isPressed) {
int line = key >> 4;
@ -794,6 +794,51 @@ class ConcreteMachine:
}
private:
inline void write_to_gate_array(uint8_t value) {
switch(value >> 6) {
case 0: crtc_bus_handler_.select_pen(value & 0x1f); break;
case 1: crtc_bus_handler_.set_colour(value & 0x1f); break;
case 2:
// Perform ROM paging.
read_pointers_[0] = (value & 4) ? write_pointers_[0] : roms_[rom_model_].data();
upper_rom_is_paged_ = !(value & 8);
read_pointers_[3] = upper_rom_is_paged_ ? roms_[upper_rom_].data() : write_pointers_[3];
// Reset the interrupt timer if requested.
if(value & 0x10) interrupt_timer_.reset_count();
// Post the next mode.
crtc_bus_handler_.set_next_mode(value & 3);
break;
case 3:
// Perform RAM paging, if 128kb is permitted.
if(has_128k_) {
bool adjust_low_read_pointer = read_pointers_[0] == write_pointers_[0];
bool adjust_high_read_pointer = read_pointers_[3] == write_pointers_[3];
#define RAM_BANK(x) &ram_[x * 16384]
#define RAM_CONFIG(a, b, c, d) write_pointers_[0] = RAM_BANK(a); write_pointers_[1] = RAM_BANK(b); write_pointers_[2] = RAM_BANK(c); write_pointers_[3] = RAM_BANK(d);
switch(value & 7) {
case 0: RAM_CONFIG(0, 1, 2, 3); break;
case 1: RAM_CONFIG(0, 1, 2, 7); break;
case 2: RAM_CONFIG(4, 5, 6, 7); break;
case 3: RAM_CONFIG(0, 3, 2, 7); break;
case 4: RAM_CONFIG(0, 4, 2, 3); break;
case 5: RAM_CONFIG(0, 5, 2, 3); break;
case 6: RAM_CONFIG(0, 6, 2, 3); break;
case 7: RAM_CONFIG(0, 7, 2, 3); break;
}
#undef RAM_CONFIG
#undef RAM_BANK
if(adjust_low_read_pointer) read_pointers_[0] = write_pointers_[0];
read_pointers_[1] = write_pointers_[1];
read_pointers_[2] = write_pointers_[2];
if(adjust_high_read_pointer) read_pointers_[3] = write_pointers_[3];
}
break;
}
}
CPU::Z80::Processor<ConcreteMachine> z80_;
CRTCBusHandler crtc_bus_handler_;
@ -836,3 +881,5 @@ using namespace AmstradCPC;
Machine *Machine::AmstradCPC() {
return new AmstradCPC::ConcreteMachine;
}
Machine::~Machine() {}

View File

@ -52,6 +52,8 @@ class Machine:
public ConfigurationTarget::Machine,
public KeyboardMachine::Machine {
public:
virtual ~Machine();
/// Creates an returns an Amstrad CPC on the heap.
static Machine *AmstradCPC();

View File

@ -0,0 +1,89 @@
//
// CharacterMapper.cpp
// Clock Signal
//
// Created by Thomas Harte on 11/08/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#include "CharacterMapper.hpp"
#include "AmstradCPC.hpp"
using namespace AmstradCPC;
uint16_t *CharacterMapper::sequence_for_character(char character) {
#define KEYS(...) {__VA_ARGS__, EndSequence}
#define SHIFT(...) {KeyShift, __VA_ARGS__, EndSequence}
#define X {NotMapped}
static KeySequence key_sequences[] = {
/* NUL */ X, /* SOH */ X,
/* STX */ X, /* ETX */ X,
/* EOT */ X, /* ENQ */ X,
/* ACK */ X, /* BEL */ X,
/* BS */ KEYS(KeyDelete), /* HT */ X,
/* LF */ KEYS(KeyReturn), /* VT */ X,
/* FF */ X, /* CR */ X,
/* SO */ X, /* SI */ X,
/* DLE */ X, /* DC1 */ X,
/* DC2 */ X, /* DC3 */ X,
/* DC4 */ X, /* NAK */ X,
/* SYN */ X, /* ETB */ X,
/* CAN */ X, /* EM */ X,
/* SUB */ X, /* ESC */ X,
/* FS */ X, /* GS */ X,
/* RS */ X, /* US */ X,
/* space */ KEYS(KeySpace), /* ! */ SHIFT(Key1),
/* " */ SHIFT(Key2), /* # */ SHIFT(Key3),
/* $ */ SHIFT(Key4), /* % */ SHIFT(Key5),
/* & */ SHIFT(Key6), /* ' */ SHIFT(Key7),
/* ( */ SHIFT(Key8), /* ) */ SHIFT(Key9),
/* * */ SHIFT(KeyColon), /* + */ SHIFT(KeySemicolon),
/* , */ KEYS(KeyComma), /* - */ KEYS(KeyMinus),
/* . */ KEYS(KeyFullStop), /* / */ KEYS(KeyForwardSlash),
/* 0 */ KEYS(Key0), /* 1 */ KEYS(Key1),
/* 2 */ KEYS(Key2), /* 3 */ KEYS(Key3),
/* 4 */ KEYS(Key4), /* 5 */ KEYS(Key5),
/* 6 */ KEYS(Key6), /* 7 */ KEYS(Key7),
/* 8 */ KEYS(Key8), /* 9 */ KEYS(Key9),
/* : */ KEYS(KeyColon), /* ; */ KEYS(KeySemicolon),
/* < */ SHIFT(KeyComma), /* = */ SHIFT(KeyMinus),
/* > */ SHIFT(KeyFullStop), /* ? */ SHIFT(KeyForwardSlash),
/* @ */ SHIFT(KeyAt), /* A */ SHIFT(KeyA),
/* B */ SHIFT(KeyB), /* C */ SHIFT(KeyC),
/* D */ SHIFT(KeyD), /* E */ SHIFT(KeyE),
/* F */ SHIFT(KeyF), /* G */ SHIFT(KeyG),
/* H */ SHIFT(KeyH), /* I */ SHIFT(KeyI),
/* J */ SHIFT(KeyJ), /* K */ SHIFT(KeyK),
/* L */ SHIFT(KeyL), /* M */ SHIFT(KeyM),
/* N */ SHIFT(KeyN), /* O */ SHIFT(KeyO),
/* P */ SHIFT(KeyP), /* Q */ SHIFT(KeyQ),
/* R */ SHIFT(KeyR), /* S */ SHIFT(KeyS),
/* T */ SHIFT(KeyT), /* U */ SHIFT(KeyU),
/* V */ SHIFT(KeyV), /* W */ SHIFT(KeyW),
/* X */ SHIFT(KeyX), /* Y */ SHIFT(KeyY),
/* Z */ SHIFT(KeyZ), /* [ */ KEYS(KeyLeftSquareBracket),
/* \ */ KEYS(KeyBackSlash), /* ] */ KEYS(KeyRightSquareBracket),
/* ^ */ SHIFT(KeyCaret), /* _ */ SHIFT(Key0),
/* ` */ X, /* a */ KEYS(KeyA),
/* b */ KEYS(KeyB), /* c */ KEYS(KeyC),
/* d */ KEYS(KeyD), /* e */ KEYS(KeyE),
/* f */ KEYS(KeyF), /* g */ KEYS(KeyG),
/* h */ KEYS(KeyH), /* i */ KEYS(KeyI),
/* j */ KEYS(KeyJ), /* k */ KEYS(KeyK),
/* l */ KEYS(KeyL), /* m */ KEYS(KeyM),
/* n */ KEYS(KeyN), /* o */ KEYS(KeyO),
/* p */ KEYS(KeyP), /* q */ KEYS(KeyQ),
/* r */ KEYS(KeyR), /* s */ KEYS(KeyS),
/* t */ KEYS(KeyT), /* u */ KEYS(KeyU),
/* v */ KEYS(KeyV), /* w */ KEYS(KeyW),
/* x */ KEYS(KeyX), /* y */ KEYS(KeyY),
/* z */ KEYS(KeyZ), /* { */ X,
/* | */ SHIFT(KeyAt), /* } */ X,
/* ~ */ X
};
#undef KEYS
#undef SHIFT
#undef X
return table_lookup_sequence_for_character(key_sequences, sizeof(key_sequences), character);
}

View File

@ -0,0 +1,23 @@
//
// CharacterMapper.hpp
// Clock Signal
//
// Created by Thomas Harte on 11/08/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#ifndef Machines_AmstradCPC_CharacterMapper_hpp
#define Machines_AmstradCPC_CharacterMapper_hpp
#include "../Typer.hpp"
namespace AmstradCPC {
class CharacterMapper: public ::Utility::CharacterMapper {
public:
uint16_t *sequence_for_character(char character);
};
}
#endif /* CharacterMapper_hpp */

View File

@ -51,7 +51,7 @@ bool Typer::try_type_next_character() {
if(!phase_) delegate_->clear_all_keys();
else {
delegate_->set_key_state(sequence[phase_ - 1], true);
return sequence[phase_] == CharacterMapper::EndSequence;
return sequence[phase_] != CharacterMapper::EndSequence;
}
return true;

View File

@ -6,8 +6,8 @@
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#ifndef CharacterMapper_hpp
#define CharacterMapper_hpp
#ifndef Machines_ZX8081_CharacterMapper_hpp
#define Machines_ZX8081_CharacterMapper_hpp
#include "../Typer.hpp"

View File

@ -384,3 +384,5 @@ using namespace ZX8081;
Machine *Machine::ZX8081() {
return new ZX8081::ConcreteMachine;
}
Machine::~Machine() {}

View File

@ -39,6 +39,7 @@ class Machine:
public KeyboardMachine::Machine {
public:
static Machine *ZX8081();
virtual ~Machine();
virtual void set_rom(ROMType type, std::vector<uint8_t> data) = 0;

View File

@ -58,6 +58,7 @@
4B3BA0D11D318B44005DD7A7 /* TestMachine6502.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B3BA0CD1D318B44005DD7A7 /* TestMachine6502.mm */; };
4B3BF5B01F146265005B6C36 /* CSW.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B3BF5AE1F146264005B6C36 /* CSW.cpp */; };
4B3F1B461E0388D200DB26EE /* PCMPatchedTrack.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B3F1B441E0388D200DB26EE /* PCMPatchedTrack.cpp */; };
4B3FE75E1F3CF68B00448EE4 /* CPM.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B3FE75C1F3CF68B00448EE4 /* CPM.cpp */; };
4B448E811F1C45A00009ABD6 /* TZX.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B448E7F1F1C45A00009ABD6 /* TZX.cpp */; };
4B448E841F1C4C480009ABD6 /* PulseQueuedTape.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B448E821F1C4C480009ABD6 /* PulseQueuedTape.cpp */; };
4B44EBF51DC987AF00A7820C /* AllSuiteA.bin in Resources */ = {isa = PBXBuildFile; fileRef = 4B44EBF41DC987AE00A7820C /* AllSuiteA.bin */; };
@ -119,6 +120,7 @@
4BAB62AD1D3272D200DF5BA0 /* Disk.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BAB62AB1D3272D200DF5BA0 /* Disk.cpp */; };
4BAB62B51D327F7E00DF5BA0 /* G64.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BAB62B31D327F7E00DF5BA0 /* G64.cpp */; };
4BAB62B81D3302CA00DF5BA0 /* PCMTrack.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BAB62B61D3302CA00DF5BA0 /* PCMTrack.cpp */; };
4BACC5B11F3DFF7C0037C015 /* CharacterMapper.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BACC5AF1F3DFF7C0037C015 /* CharacterMapper.cpp */; };
4BB17D4E1ED7909F00ABD1E1 /* tests.expected.json in Resources */ = {isa = PBXBuildFile; fileRef = 4BB17D4C1ED7909F00ABD1E1 /* tests.expected.json */; };
4BB17D4F1ED7909F00ABD1E1 /* tests.in.json in Resources */ = {isa = PBXBuildFile; fileRef = 4BB17D4D1ED7909F00ABD1E1 /* tests.in.json */; };
4BB298F11B587D8400A49093 /* start in Resources */ = {isa = PBXBuildFile; fileRef = 4BB297E51B587D8300A49093 /* start */; };
@ -562,6 +564,8 @@
4B3BF5AF1F146264005B6C36 /* CSW.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CSW.hpp; sourceTree = "<group>"; };
4B3F1B441E0388D200DB26EE /* PCMPatchedTrack.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PCMPatchedTrack.cpp; sourceTree = "<group>"; };
4B3F1B451E0388D200DB26EE /* PCMPatchedTrack.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = PCMPatchedTrack.hpp; sourceTree = "<group>"; };
4B3FE75C1F3CF68B00448EE4 /* CPM.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CPM.cpp; path = Parsers/CPM.cpp; sourceTree = "<group>"; };
4B3FE75D1F3CF68B00448EE4 /* CPM.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = CPM.hpp; path = Parsers/CPM.hpp; sourceTree = "<group>"; };
4B448E7F1F1C45A00009ABD6 /* TZX.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TZX.cpp; sourceTree = "<group>"; };
4B448E801F1C45A00009ABD6 /* TZX.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TZX.hpp; sourceTree = "<group>"; };
4B448E821F1C4C480009ABD6 /* PulseQueuedTape.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PulseQueuedTape.cpp; sourceTree = "<group>"; };
@ -667,6 +671,8 @@
4BAB62B41D327F7E00DF5BA0 /* G64.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = G64.hpp; sourceTree = "<group>"; };
4BAB62B61D3302CA00DF5BA0 /* PCMTrack.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = PCMTrack.cpp; sourceTree = "<group>"; };
4BAB62B71D3302CA00DF5BA0 /* PCMTrack.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = PCMTrack.hpp; sourceTree = "<group>"; };
4BACC5AF1F3DFF7C0037C015 /* CharacterMapper.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CharacterMapper.cpp; path = AmstradCPC/CharacterMapper.cpp; sourceTree = "<group>"; };
4BACC5B01F3DFF7C0037C015 /* CharacterMapper.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = CharacterMapper.hpp; path = AmstradCPC/CharacterMapper.hpp; sourceTree = "<group>"; };
4BB06B211F316A3F00600C7A /* ForceInline.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ForceInline.h; sourceTree = "<group>"; };
4BB17D4C1ED7909F00ABD1E1 /* tests.expected.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = tests.expected.json; path = FUSE/tests.expected.json; sourceTree = "<group>"; };
4BB17D4D1ED7909F00ABD1E1 /* tests.in.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = tests.in.json; path = FUSE/tests.in.json; sourceTree = "<group>"; };
@ -1294,6 +1300,8 @@
children = (
4B38F3461F2EC11D00D9235D /* AmstradCPC.cpp */,
4B38F3471F2EC11D00D9235D /* AmstradCPC.hpp */,
4BACC5AF1F3DFF7C0037C015 /* CharacterMapper.cpp */,
4BACC5B01F3DFF7C0037C015 /* CharacterMapper.hpp */,
);
name = AmstradCPC;
sourceTree = "<group>";
@ -1330,6 +1338,15 @@
path = Bridges;
sourceTree = "<group>";
};
4B3FE75F1F3CF6BA00448EE4 /* Parsers */ = {
isa = PBXGroup;
children = (
4B3FE75C1F3CF68B00448EE4 /* CPM.cpp */,
4B3FE75D1F3CF68B00448EE4 /* CPM.hpp */,
);
name = Parsers;
sourceTree = "<group>";
};
4B4A762D1DB1A35C007AAE2E /* AY38910 */ = {
isa = PBXGroup;
children = (
@ -1446,12 +1463,12 @@
4B69FB3A1C4D908A00B5F0AA /* Tape */ = {
isa = PBXGroup;
children = (
4B448E821F1C4C480009ABD6 /* PulseQueuedTape.cpp */,
4B69FB3B1C4D908A00B5F0AA /* Tape.cpp */,
4B448E831F1C4C480009ABD6 /* PulseQueuedTape.hpp */,
4B69FB3C1C4D908A00B5F0AA /* Tape.hpp */,
4B69FB411C4D941400B5F0AA /* Formats */,
4B8805F11DCFC9A2003085B1 /* Parsers */,
4B448E821F1C4C480009ABD6 /* PulseQueuedTape.cpp */,
4B448E831F1C4C480009ABD6 /* PulseQueuedTape.hpp */,
4B69FB3B1C4D908A00B5F0AA /* Tape.cpp */,
4B69FB3C1C4D908A00B5F0AA /* Tape.hpp */,
);
path = Tape;
sourceTree = "<group>";
@ -1545,6 +1562,7 @@
4BAB62B71D3302CA00DF5BA0 /* PCMTrack.hpp */,
4BB697CF1D4BA44900248BDF /* Encodings */,
4BAB62B21D327F7E00DF5BA0 /* Formats */,
4B3FE75F1F3CF6BA00448EE4 /* Parsers */,
);
path = Disk;
sourceTree = "<group>";
@ -2700,6 +2718,7 @@
4B1497921EE4B5A800CE2596 /* ZX8081.cpp in Sources */,
4B643F3F1D77B88000D431D6 /* DocumentController.swift in Sources */,
4BA799951D8B656E0045123D /* StaticAnalyser.cpp in Sources */,
4B3FE75E1F3CF68B00448EE4 /* CPM.cpp in Sources */,
4B2BFDB21DAEF5FF001A68B8 /* Video.cpp in Sources */,
4B4DC82B1D2C27A4003C5BF8 /* SerialBus.cpp in Sources */,
4BC3B74F1CD194CC00F86E85 /* Shader.cpp in Sources */,
@ -2710,6 +2729,7 @@
4B96F7221D75119A0058BB2D /* Tape.cpp in Sources */,
4B0BE4281D3481E700D5256B /* DigitalPhaseLockedLoop.cpp in Sources */,
4B8805F71DCFF6C9003085B1 /* Commodore.cpp in Sources */,
4BACC5B11F3DFF7C0037C015 /* CharacterMapper.cpp in Sources */,
4BD69F941D98760000243FE1 /* AcornADF.cpp in Sources */,
4BBF99181C8FBA6F0075DAFB /* TextureTarget.cpp in Sources */,
4BC76E691C98E31700E6EF73 /* FIRFilter.cpp in Sources */,

View File

@ -116,6 +116,9 @@ struct MachineDelegate: CRTMachine::Machine::Delegate {
- (void)setupOutputWithAspectRatio:(float)aspectRatio {
self.machine->setup_output(aspectRatio);
// Since OS X v10.6, Macs have had a gamma of 2.2.
self.machine->get_crt()->set_output_gamma(2.2f);
}
- (void)drawViewForPixelSize:(CGSize)pixelSize onlyIfDirty:(BOOL)onlyIfDirty {

View File

@ -56,10 +56,12 @@ void CRT::set_new_display_type(unsigned int cycles_per_line, DisplayType display
switch(displayType) {
case DisplayType::PAL50:
set_new_timing(cycles_per_line, 312, ColourSpace::YUV, 709379, 2500, 5, true); // i.e. 283.7516; 2.5 lines = vertical sync
set_input_gamma(2.8f);
break;
case DisplayType::NTSC60:
set_new_timing(cycles_per_line, 262, ColourSpace::YIQ, 455, 2, 6, false); // i.e. 227.5, 3 lines = vertical sync
set_input_gamma(2.2f);
break;
}
}
@ -72,6 +74,21 @@ void CRT::set_composite_function_type(CompositeSourceType type, float offset_of_
}
}
void CRT::set_input_gamma(float gamma) {
input_gamma_ = gamma;
update_gamma();
}
void CRT::set_output_gamma(float gamma) {
output_gamma_ = gamma;
update_gamma();
}
void CRT::update_gamma() {
float gamma_ratio = input_gamma_ / output_gamma_;
openGL_output_builder_.set_gamma(gamma_ratio);
}
CRT::CRT(unsigned int common_output_divisor, unsigned int buffer_depth) :
is_receiving_sync_(false),
common_output_divisor_(common_output_divisor),

View File

@ -97,6 +97,9 @@ class CRT {
unsigned int cycles_per_line_;
float input_gamma_, output_gamma_;
void update_gamma();
public:
/*! Constructs the CRT with a specified clock rate, height and colour subcarrier frequency.
The requested number of buffers, each with the requested number of bytes per pixel,
@ -238,6 +241,12 @@ class CRT {
openGL_output_builder_.draw_frame(output_width, output_height, only_if_dirty);
}
/*! Sets the gamma exponent for the simulated screen. */
void set_input_gamma(float gamma);
/*! Sets the gamma exponent for the real, tangible screen on which content will be drawn. */
void set_output_gamma(float gamma);
/*! Tells the CRT that the next call to draw_frame will occur on a different OpenGL context than
the previous.

View File

@ -89,6 +89,7 @@ void OpenGLOutputBuilder::draw_frame(unsigned int output_width, unsigned int out
set_timing_uniforms();
set_colour_space_uniforms();
set_gamma();
}
if(fence_ != nullptr) {
@ -375,6 +376,10 @@ void OpenGLOutputBuilder::set_colour_space_uniforms() {
if(composite_chrominance_filter_shader_program_) composite_chrominance_filter_shader_program_->set_colour_conversion_matrices(fromRGB, toRGB);
}
void OpenGLOutputBuilder::set_gamma() {
if(output_shader_program_) output_shader_program_->set_gamma_ratio(gamma_);
}
float OpenGLOutputBuilder::get_composite_output_width() const {
return ((float)colour_cycle_numerator_ * 4.0f) / (float)(colour_cycle_denominator_ * IntermediateBufferWidth);
}

View File

@ -34,6 +34,7 @@ class OpenGLOutputBuilder {
unsigned int colour_cycle_numerator_;
unsigned int colour_cycle_denominator_;
OutputDevice output_device_;
float gamma_;
// timing information to allow reasoning about input information
unsigned int input_frequency_;
@ -89,6 +90,7 @@ class OpenGLOutputBuilder {
void set_timing_uniforms();
void set_colour_space_uniforms();
void set_gamma();
void establish_OpenGL_state();
void reset_all_OpenGL_state();
@ -118,6 +120,11 @@ class OpenGLOutputBuilder {
visible_area_ = visible_area;
}
inline void set_gamma(float gamma) {
gamma_ = gamma;
set_gamma();
}
inline std::unique_lock<std::mutex> get_output_lock() {
return std::unique_lock<std::mutex>(output_mutex_);
}

View File

@ -74,12 +74,13 @@ std::unique_ptr<OutputShader> OutputShader::make_shader(const char *fragment_met
"out vec4 fragColour;"
"uniform %s texID;"
"uniform float gamma;"
"\n%s\n"
"void main(void)"
"{"
"fragColour = vec4(%s, 0.5);"//*cos(lateralVarying)
"fragColour = vec4(pow(%s, vec3(gamma)), 0.5);"//*cos(lateralVarying)
"}",
sampler_type, fragment_methods, colour_expression);
@ -116,6 +117,10 @@ void OutputShader::set_timing(unsigned int height_of_display, unsigned int cycle
set_uniform("positionConversion", (GLfloat)horizontal_scan_period, (GLfloat)vertical_scan_period / (GLfloat)vertical_period_divider);
}
void OutputShader::set_gamma_ratio(float ratio) {
set_uniform("gamma", ratio);
}
void OutputShader::set_input_width_scaler(float input_scaler) {
set_uniform("inputScaler", input_scaler);
}

View File

@ -63,6 +63,8 @@ public:
*/
void set_origin_is_double_height(bool is_double_height);
void set_gamma_ratio(float ratio);
/*!
Sets the proportion of the input area that should be considered the whole width 1.0 means use all available
space, 0.5 means use half, etc.

View File

@ -9,9 +9,10 @@ So its aims are:
It currently contains emulations of the:
* Acorn Electron;
* Amstrad CPC;
* Atari 2600;
* Oric 1/Atmos;
* Commodore Vic-20 (and Commodore 1540/1); and
* Commodore Vic-20 (and Commodore 1540/1);
* Oric 1/Atmos; and
* Sinclair ZX80/81.
## Single-click Loading

View File

@ -19,8 +19,8 @@ std::unique_ptr<Catalogue> StaticAnalyser::Acorn::GetDFSCatalogue(const std::sha
std::unique_ptr<Catalogue> catalogue(new Catalogue);
Storage::Encodings::MFM::Parser parser(false, disk);
std::shared_ptr<Storage::Encodings::MFM::Sector> names = parser.get_sector(0, 0);
std::shared_ptr<Storage::Encodings::MFM::Sector> details = parser.get_sector(0, 1);
std::shared_ptr<Storage::Encodings::MFM::Sector> names = parser.get_sector(0, 0, 0);
std::shared_ptr<Storage::Encodings::MFM::Sector> details = parser.get_sector(0, 0, 1);
if(!names || !details) return nullptr;
if(names->data.size() != 256 || details->data.size() != 256) return nullptr;
@ -61,7 +61,7 @@ std::unique_ptr<Catalogue> StaticAnalyser::Acorn::GetDFSCatalogue(const std::sha
uint8_t track = (uint8_t)(start_sector / 10);
start_sector++;
std::shared_ptr<Storage::Encodings::MFM::Sector> next_sector = parser.get_sector(track, sector);
std::shared_ptr<Storage::Encodings::MFM::Sector> next_sector = parser.get_sector(0, track, sector);
if(!next_sector) break;
long length_from_sector = std::min(data_length, 256l);
@ -77,13 +77,13 @@ std::unique_ptr<Catalogue> StaticAnalyser::Acorn::GetADFSCatalogue(const std::sh
std::unique_ptr<Catalogue> catalogue(new Catalogue);
Storage::Encodings::MFM::Parser parser(true, disk);
std::shared_ptr<Storage::Encodings::MFM::Sector> free_space_map_second_half = parser.get_sector(0, 1);
std::shared_ptr<Storage::Encodings::MFM::Sector> free_space_map_second_half = parser.get_sector(0, 0, 1);
if(!free_space_map_second_half) return nullptr;
std::vector<uint8_t> root_directory;
root_directory.reserve(5 * 256);
for(uint8_t c = 2; c < 7; c++) {
std::shared_ptr<Storage::Encodings::MFM::Sector> sector = parser.get_sector(0, c);
std::shared_ptr<Storage::Encodings::MFM::Sector> sector = parser.get_sector(0, 0, c);
if(!sector) return nullptr;
root_directory.insert(root_directory.end(), sector->data.begin(), sector->data.end());
}

View File

@ -7,6 +7,50 @@
//
#include "StaticAnalyser.hpp"
#include "../../Storage/Disk/Parsers/CPM.hpp"
static void InspectDataCatalogue(
const std::unique_ptr<Storage::Disk::CPM::Catalogue> &data_catalogue,
StaticAnalyser::Target &target) {
// If there's just one file, run that.
if(data_catalogue->files.size() == 1) {
target.loadingCommand = "run\"" + data_catalogue->files[0].name + "\n";
return;
}
// If only one file is [potentially] BASIC, run that one; otherwise if only one has no suffix,
// pick that one.
int basic_files = 0;
int nonsuffixed_files = 0;
size_t last_basic_file = 0;
size_t last_nonsuffixed_file = 0;
for(size_t c = 0; c < data_catalogue->files.size(); c++) {
// Check for whether this is [potentially] BASIC.
if(!((data_catalogue->files[c].data[18] >> 1) & 7)) {
basic_files++;
last_basic_file = c;
}
// Check suffix for emptiness.
if(data_catalogue->files[c].type == " ") {
nonsuffixed_files++;
last_nonsuffixed_file = c;
}
}
if(basic_files == 1 || nonsuffixed_files == 1) {
size_t selected_file = (basic_files == 1) ? last_basic_file : last_nonsuffixed_file;
target.loadingCommand = "run\"" + data_catalogue->files[selected_file].name + "\n";
return;
}
// Desperation.
target.loadingCommand = "cat\n";
}
static void InspectSystemCatalogue(
const std::unique_ptr<Storage::Disk::CPM::Catalogue> &data_catalogue,
StaticAnalyser::Target &target) {
}
void StaticAnalyser::AmstradCPC::AddTargets(
const std::list<std::shared_ptr<Storage::Disk::Disk>> &disks,
@ -20,7 +64,42 @@ void StaticAnalyser::AmstradCPC::AddTargets(
target.tapes = tapes;
target.cartridges = cartridges;
target.amstradcpc.model = target.disks.empty() ? AmstradCPCModel::CPC464 : AmstradCPCModel::CPC6128;
target.amstradcpc.model = AmstradCPCModel::CPC6128;
if(!target.tapes.empty()) {
// Ugliness flows here: assume the CPC isn't smart enough to pause between pressing
// enter and responding to the follow-on prompt to press a key, so just type for
// a while. Yuck!
target.loadingCommand = "|tape\nrun\"\n1234567890";
}
if(!target.disks.empty()) {
Storage::Disk::CPM::ParameterBlock data_format;
data_format.sectors_per_track = 9;
data_format.tracks = 40;
data_format.block_size = 1024;
data_format.first_sector = 0xc1;
data_format.catalogue_allocation_bitmap = 0xc000;
data_format.reserved_tracks = 0;
std::unique_ptr<Storage::Disk::CPM::Catalogue> data_catalogue = Storage::Disk::CPM::GetCatalogue(target.disks.front(), data_format);
if(data_catalogue) {
InspectDataCatalogue(data_catalogue, target);
} else {
Storage::Disk::CPM::ParameterBlock system_format;
data_format.sectors_per_track = 9;
data_format.tracks = 40;
data_format.block_size = 1024;
data_format.first_sector = 0x41;
data_format.catalogue_allocation_bitmap = 0xc000;
data_format.reserved_tracks = 2;
std::unique_ptr<Storage::Disk::CPM::Catalogue> system_catalogue = Storage::Disk::CPM::GetCatalogue(target.disks.front(), system_format);
if(system_catalogue) {
InspectSystemCatalogue(data_catalogue, target);
}
}
}
destination.push_back(target);
}

View File

@ -46,6 +46,11 @@ bool Drive::get_is_read_only() {
return false;
}
bool Drive::get_is_ready() {
// TODO: a real test for this.
return disk_ != nullptr;
}
std::shared_ptr<Track> Drive::get_track() {
if(disk_) return disk_->get_track_at_position(head_, (unsigned int)head_position_);
if(track_) return track_;

View File

@ -65,6 +65,11 @@ class Drive {
*/
void set_track(const std::shared_ptr<Track> &track);
/*!
@returns @c true if the drive is ready; @c false otherwise.
*/
bool get_is_ready();
private:
std::shared_ptr<Track> track_;
std::shared_ptr<Disk> disk_;

View File

@ -114,22 +114,12 @@ class FMEncoder: public Encoder {
}
};
static uint8_t logarithmic_size_for_size(size_t size) {
switch(size) {
default: return 0;
case 256: return 1;
case 512: return 2;
case 1024: return 3;
case 2048: return 4;
case 4196: return 5;
}
}
template<class T> std::shared_ptr<Storage::Disk::Track>
GetTrackWithSectors(
const std::vector<Sector> &sectors,
size_t post_index_address_mark_bytes, uint8_t post_index_address_mark_value,
size_t pre_address_mark_bytes, size_t post_address_mark_bytes,
size_t pre_address_mark_bytes,
size_t post_address_mark_bytes, uint8_t post_address_mark_value,
size_t pre_data_mark_bytes, size_t post_data_bytes,
size_t inter_sector_gap,
size_t expected_track_bytes) {
@ -153,21 +143,30 @@ template<class T> std::shared_ptr<Storage::Disk::Track>
shifter.add_byte(sector.track);
shifter.add_byte(sector.side);
shifter.add_byte(sector.sector);
uint8_t size = logarithmic_size_for_size(sector.data.size());
shifter.add_byte(size);
shifter.add_crc();
shifter.add_byte(sector.size);
shifter.add_crc(sector.has_header_crc_error);
// gap
for(size_t c = 0; c < post_address_mark_bytes; c++) shifter.add_byte(0x4e);
for(size_t c = 0; c < post_address_mark_bytes; c++) shifter.add_byte(post_address_mark_value);
for(size_t c = 0; c < pre_data_mark_bytes; c++) shifter.add_byte(0x00);
// data
// data, if attached
if(!sector.data.empty()) {
if(sector.is_deleted)
shifter.add_deleted_data_address_mark();
else
shifter.add_data_address_mark();
for(size_t c = 0; c < sector.data.size(); c++)
{
size_t c = 0;
size_t declared_length = (size_t)(128 << sector.size);
for(c = 0; c < sector.data.size() && c < declared_length; c++) {
shifter.add_byte(sector.data[c]);
}
shifter.add_crc();
for(; c < declared_length; c++) {
shifter.add_byte(0x00);
}
shifter.add_crc(sector.has_data_crc_error);
}
// gap
for(size_t c = 0; c < post_data_bytes; c++) shifter.add_byte(0x00);
@ -189,28 +188,32 @@ void Encoder::output_short(uint16_t value) {
target_.push_back(value & 0xff);
}
void Encoder::add_crc() {
void Encoder::add_crc(bool incorrectly) {
uint16_t crc_value = crc_generator_.get_value();
add_byte(crc_value >> 8);
add_byte(crc_value & 0xff);
add_byte((crc_value & 0xff) ^ (incorrectly ? 1 : 0));
}
std::shared_ptr<Storage::Disk::Track> Storage::Encodings::MFM::GetFMTrackWithSectors(const std::vector<Sector> &sectors) {
const size_t Storage::Encodings::MFM::DefaultSectorGapLength = (size_t)~0;
std::shared_ptr<Storage::Disk::Track> Storage::Encodings::MFM::GetFMTrackWithSectors(const std::vector<Sector> &sectors, size_t sector_gap_length, uint8_t sector_gap_filler_byte) {
return GetTrackWithSectors<FMEncoder>(
sectors,
16, 0x00,
6, 0,
17, 14,
6,
(sector_gap_length != DefaultSectorGapLength) ? sector_gap_length : 0, sector_gap_filler_byte,
(sector_gap_length != DefaultSectorGapLength) ? 0 : 17, 14,
0,
6250); // i.e. 250kbps (including clocks) * 60 = 15000kpm, at 300 rpm => 50 kbits/rotation => 6250 bytes/rotation
}
std::shared_ptr<Storage::Disk::Track> Storage::Encodings::MFM::GetMFMTrackWithSectors(const std::vector<Sector> &sectors) {
std::shared_ptr<Storage::Disk::Track> Storage::Encodings::MFM::GetMFMTrackWithSectors(const std::vector<Sector> &sectors, size_t sector_gap_length, uint8_t sector_gap_filler_byte) {
return GetTrackWithSectors<MFMEncoder>(
sectors,
50, 0x4e,
12, 22,
12, 18,
12,
(sector_gap_length != DefaultSectorGapLength) ? sector_gap_length : 22, sector_gap_filler_byte,
(sector_gap_length != DefaultSectorGapLength) ? 0 : 12, 18,
32,
12500); // unintelligently: double the single-density bytes/rotation (or: 500kps @ 300 rpm)
}
@ -228,25 +231,26 @@ std::unique_ptr<Encoder> Storage::Encodings::MFM::GetFMEncoder(std::vector<uint8
Parser::Parser(bool is_mfm) :
Storage::Disk::Controller(4000000, 1, 300),
crc_generator_(0x1021, 0xffff),
shift_register_(0), track_(0), is_mfm_(is_mfm) {
shift_register_(0), is_mfm_(is_mfm),
track_(0), head_(0) {
Storage::Time bit_length;
bit_length.length = 1;
bit_length.clock_rate = is_mfm ? 500000 : 250000; // i.e. 250 kbps (including clocks)
set_expected_bit_length(bit_length);
drive.reset(new Storage::Disk::Drive);
set_drive(drive);
drive_.reset(new Storage::Disk::Drive);
set_drive(drive_);
set_motor_on(true);
}
Parser::Parser(bool is_mfm, const std::shared_ptr<Storage::Disk::Disk> &disk) :
Parser(is_mfm) {
drive->set_disk(disk);
drive_->set_disk(disk);
}
Parser::Parser(bool is_mfm, const std::shared_ptr<Storage::Disk::Track> &track) :
Parser(is_mfm) {
drive->set_disk_with_track(track);
drive_->set_disk_with_track(track);
}
void Parser::seek_to_track(uint8_t track) {
@ -261,7 +265,20 @@ void Parser::seek_to_track(uint8_t track) {
}
}
std::shared_ptr<Sector> Parser::get_sector(uint8_t track, uint8_t sector) {
std::shared_ptr<Sector> Parser::get_sector(uint8_t head, uint8_t track, uint8_t sector) {
// Check cache for sector.
int index = get_index(head, track, sector);
auto cached_sector = sectors_by_index_.find(index);
if(cached_sector != sectors_by_index_.end()) {
return cached_sector->second;
}
// Failing that, set the proper head and track, and search for the sector. get_sector automatically
// inserts everything found into sectors_by_index_.
if(head_ != head) {
drive_->set_head(head);
invalidate_track();
}
seek_to_track(track);
return get_sector(sector);
}
@ -384,8 +401,7 @@ std::vector<uint8_t> Parser::get_track() {
}
std::shared_ptr<Sector> Parser::get_next_sector()
{
std::shared_ptr<Sector> Parser::get_next_sector() {
std::shared_ptr<Sector> sector(new Sector);
index_count_ = 0;
@ -455,6 +471,10 @@ std::shared_ptr<Sector> Parser::get_next_sector()
if((data_crc >> 8) != get_next_byte()) continue;
if((data_crc & 0xff) != get_next_byte()) continue;
// Put this sector into the cache.
int index = get_index(head_, track_, sector->sector);
sectors_by_index_[index] = sector;
return sector;
}
@ -465,7 +485,7 @@ std::shared_ptr<Sector> Parser::get_sector(uint8_t sector) {
std::shared_ptr<Sector> first_sector;
index_count_ = 0;
while(!first_sector && index_count_ < 2) first_sector = get_next_sector();
if(!first_sector) return first_sector;
if(!first_sector) return nullptr;
if(first_sector->sector == sector) return first_sector;
while(1) {
@ -475,3 +495,7 @@ std::shared_ptr<Sector> Parser::get_sector(uint8_t sector) {
if(next_sector->sector == sector) return next_sector;
}
}
int Parser::get_index(uint8_t head, uint8_t track, uint8_t sector) {
return head | (track << 8) | (sector << 16);
}

View File

@ -36,13 +36,39 @@ const uint16_t MFMPostSyncCRCValue = 0xcdb4; // the value the CRC generator sho
const uint8_t MFMIndexSyncByteValue = 0xc2;
const uint8_t MFMSyncByteValue = 0xa1;
/*!
Represents a single [M]FM sector, identified by its track, side and sector records, a blob of data
and a few extra flags of metadata.
*/
struct Sector {
uint8_t track, side, sector;
uint8_t track, side, sector, size;
std::vector<uint8_t> data;
bool has_data_crc_error;
bool has_header_crc_error;
bool is_deleted;
Sector() : track(0), side(0), sector(0), size(0), has_data_crc_error(false), has_header_crc_error(false), is_deleted(false) {}
};
std::shared_ptr<Storage::Disk::Track> GetMFMTrackWithSectors(const std::vector<Sector> &sectors);
std::shared_ptr<Storage::Disk::Track> GetFMTrackWithSectors(const std::vector<Sector> &sectors);
extern const size_t DefaultSectorGapLength;
/*!
Converts a vector of sectors into a properly-encoded MFM track.
@param sectors The sectors to write.
@param sector_gap_length If specified, sets the distance in whole bytes between each ID and its data.
@param sector_gap_filler_byte If specified, sets the value (unencoded) that is used to populate the gap between each ID and its data.
*/
std::shared_ptr<Storage::Disk::Track> GetMFMTrackWithSectors(const std::vector<Sector> &sectors, size_t sector_gap_length = DefaultSectorGapLength, uint8_t sector_gap_filler_byte = 0x4e);
/*!
Converts a vector of sectors into a properly-encoded FM track.
@param sectors The sectors to write.
@param sector_gap_length If specified, sets the distance in whole bytes between each ID and its data.
@param sector_gap_filler_byte If specified, sets the value (unencoded) that is used to populate the gap between each ID and its data.
*/
std::shared_ptr<Storage::Disk::Track> GetFMTrackWithSectors(const std::vector<Sector> &sectors, size_t sector_gap_length = DefaultSectorGapLength, uint8_t sector_gap_filler_byte = 0x4e);
class Encoder {
public:
@ -53,7 +79,9 @@ class Encoder {
virtual void add_data_address_mark() = 0;
virtual void add_deleted_data_address_mark() = 0;
virtual void output_short(uint16_t value);
void add_crc();
/// Outputs the CRC for all data since the last address mask; if @c incorrectly is @c true then outputs an incorrect CRC.
void add_crc(bool incorrectly);
protected:
NumberTheory::CRC16 crc_generator_;
@ -75,7 +103,7 @@ class Parser: public Storage::Disk::Controller {
@returns a sector if one was found; @c nullptr otherwise.
*/
std::shared_ptr<Storage::Encodings::MFM::Sector> get_sector(uint8_t track, uint8_t sector);
std::shared_ptr<Storage::Encodings::MFM::Sector> get_sector(uint8_t head, uint8_t track, uint8_t sector);
/*!
Attempts to read the track at @c track, starting from the index hole.
@ -92,10 +120,10 @@ class Parser: public Storage::Disk::Controller {
private:
Parser(bool is_mfm);
std::shared_ptr<Storage::Disk::Drive> drive;
std::shared_ptr<Storage::Disk::Drive> drive_;
unsigned int shift_register_;
int index_count_;
uint8_t track_;
uint8_t track_, head_;
int bit_count_;
NumberTheory::CRC16 crc_generator_;
bool is_mfm_;
@ -110,6 +138,9 @@ class Parser: public Storage::Disk::Controller {
std::shared_ptr<Storage::Encodings::MFM::Sector> get_next_sector();
std::shared_ptr<Storage::Encodings::MFM::Sector> get_sector(uint8_t sector);
std::vector<uint8_t> get_track();
std::map<int, std::shared_ptr<Storage::Encodings::MFM::Sector>> sectors_by_index_;
int get_index(uint8_t head, uint8_t track, uint8_t sector);
};

View File

@ -14,6 +14,7 @@
namespace {
static const unsigned int sectors_per_track = 16;
static const unsigned int bytes_per_sector = 256;
static const unsigned int sector_size = 1;
}
using namespace Storage::Disk;
@ -69,6 +70,7 @@ std::shared_ptr<Track> AcornADF::get_uncached_track_at_position(unsigned int hea
new_sector.track = (uint8_t)position;
new_sector.side = (uint8_t)head;
new_sector.sector = (uint8_t)sector;
new_sector.size = sector_size;
new_sector.data.resize(bytes_per_sector);
fread(&new_sector.data[0], 1, bytes_per_sector, file_);
@ -87,7 +89,7 @@ void AcornADF::store_updated_track_at_position(unsigned int head, unsigned int p
std::vector<uint8_t> parsed_track;
Storage::Encodings::MFM::Parser parser(true, track);
for(unsigned int c = 0; c < sectors_per_track; c++) {
std::shared_ptr<Storage::Encodings::MFM::Sector> sector = parser.get_sector((uint8_t)position, (uint8_t)c);
std::shared_ptr<Storage::Encodings::MFM::Sector> sector = parser.get_sector(0, (uint8_t)position, (uint8_t)c);
if(sector) {
parsed_track.insert(parsed_track.end(), sector->data.begin(), sector->data.end());
} else {

View File

@ -75,8 +75,8 @@ std::shared_ptr<Track> CPCDSK::get_uncached_track_at_position(unsigned int head,
// Grab the track information.
fseek(file_, 5, SEEK_CUR); // skip track number, side number, sector size — each is given per sector
int number_of_sectors = fgetc(file_);
__unused uint8_t gap3_length = (uint8_t)fgetc(file_);
__unused uint8_t filler_byte = (uint8_t)fgetc(file_);
uint8_t gap3_length = (uint8_t)fgetc(file_);
uint8_t filler_byte = (uint8_t)fgetc(file_);
// Grab the sector information
struct SectorInfo {
@ -111,6 +111,7 @@ std::shared_ptr<Track> CPCDSK::get_uncached_track_at_position(unsigned int head,
new_sector.track = sector_info.track;
new_sector.side = sector_info.side;
new_sector.sector = sector_info.sector;
new_sector.size = sector_info.length;
size_t data_size;
if(is_extended_) {
@ -122,32 +123,34 @@ std::shared_ptr<Track> CPCDSK::get_uncached_track_at_position(unsigned int head,
new_sector.data.resize(data_size);
fread(new_sector.data.data(), sizeof(uint8_t), data_size, file_);
// TODO: obey the status bytes, somehow (?)
if(sector_info.status1 || sector_info.status2) {
if(sector_info.status1 & 0x08) {
// The CRC failed in the ID field.
new_sector.has_header_crc_error = true;
}
if(sector_info.status2 & 0x20) {
// The CRC failed in the data field.
new_sector.has_data_crc_error = true;
}
if(sector_info.status2 & 0x40) {
// This sector is marked as deleted.
new_sector.is_deleted = true;
}
if(sector_info.status2 & 0x01) {
// Data field wasn't found.
}
printf("Unhandled: status errors\n");
new_sector.data.clear();
}
sectors.push_back(std::move(new_sector));
}
// TODO: supply gay 3 length and filler byte
if(sectors.size()) return Storage::Encodings::MFM::GetMFMTrackWithSectors(sectors);
// TODO: extensions to the extended format; John Elliot's addition of single-density support,
// and Simon Owen's weak/random sectors, subject to adding some logic to pick a potential
// FM/MFM encoding that can produce specified weak values.
if(sectors.size()) return Storage::Encodings::MFM::GetMFMTrackWithSectors(sectors, gap3_length, filler_byte);
return nullptr;
}

View File

@ -60,6 +60,7 @@ std::shared_ptr<Track> SSD::get_uncached_track_at_position(unsigned int head, un
new_sector.track = (uint8_t)position;
new_sector.side = 0;
new_sector.sector = (uint8_t)sector;
new_sector.size = 1;
new_sector.data.resize(256);
fread(new_sector.data.data(), 1, 256, file_);
@ -81,7 +82,7 @@ void SSD::store_updated_track_at_position(unsigned int head, unsigned int positi
std::vector<uint8_t> parsed_track;
Storage::Encodings::MFM::Parser parser(false, track);
for(unsigned int c = 0; c < 10; c++) {
std::shared_ptr<Storage::Encodings::MFM::Sector> sector = parser.get_sector((uint8_t)position, (uint8_t)c);
std::shared_ptr<Storage::Encodings::MFM::Sector> sector = parser.get_sector(0, (uint8_t)position, (uint8_t)c);
if(sector) {
parsed_track.insert(parsed_track.end(), sector->data.begin(), sector->data.end());
} else {

View File

@ -0,0 +1,120 @@
//
// CPM.cpp
// Clock Signal
//
// Created by Thomas Harte on 10/08/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#include "CPM.hpp"
#include "../Encodings/MFM.hpp"
using namespace Storage::Disk::CPM;
std::unique_ptr<Storage::Disk::CPM::Catalogue> Storage::Disk::CPM::GetCatalogue(const std::shared_ptr<Storage::Disk::Disk> &disk, const ParameterBlock &parameters) {
Storage::Encodings::MFM::Parser parser(true, disk);
// Assemble the actual bytes of the catalogue.
std::vector<uint8_t> catalogue;
size_t sector_size = 1;
uint16_t catalogue_allocation_bitmap = parameters.catalogue_allocation_bitmap;
if(!catalogue_allocation_bitmap) return nullptr;
int sector = 0;
int track = parameters.reserved_tracks;
while(catalogue_allocation_bitmap) {
if(catalogue_allocation_bitmap & 0x8000) {
std::shared_ptr<Storage::Encodings::MFM::Sector> sector_contents = parser.get_sector(0, (uint8_t)track, (uint8_t)(parameters.first_sector + sector));
if(!sector_contents) {
return nullptr;
}
catalogue.insert(catalogue.end(), sector_contents->data.begin(), sector_contents->data.end());
sector_size = sector_contents->data.size();
}
catalogue_allocation_bitmap <<= 1;
sector++;
if(sector == parameters.sectors_per_track) {
sector = 0;
track++;
}
}
std::unique_ptr<Catalogue> result(new Catalogue);
bool has_long_allocation_units = (parameters.tracks * parameters.sectors_per_track * (int)sector_size / parameters.block_size) >= 256;
size_t bytes_per_catalogue_entry = (has_long_allocation_units ? 16 : 8) * (size_t)parameters.block_size;
// From the catalogue, create files.
std::map<std::vector<uint8_t>, size_t> indices_by_name;
File empty_file;
for(size_t c = 0; c < catalogue.size(); c += 32) {
// Skip this file if it's deleted; this is marked by it having 0xe5 as its user number
if(catalogue[c] == 0xe5) continue;
// Check whether this file has yet been seen; if not then add it to the list
std::vector<uint8_t> descriptor;
size_t index;
descriptor.insert(descriptor.begin(), &catalogue[c], &catalogue[c + 12]);
auto iterator = indices_by_name.find(descriptor);
if(iterator != indices_by_name.end()) {
index = iterator->second;
} else {
File new_file;
new_file.user_number = catalogue[c];
for(size_t s = 0; s < 8; s++) new_file.name.push_back((char)catalogue[c + s + 1]);
for(size_t s = 0; s < 3; s++) new_file.type.push_back((char)catalogue[c + s + 9] & 0x7f);
new_file.read_only = catalogue[c + 9] & 0x80;
new_file.system = catalogue[c + 10] & 0x80;
index = result->files.size();
result->files.push_back(new_file);
indices_by_name[descriptor] = index;
}
// figure out where this data needs to be pasted in
size_t extent = (size_t)(catalogue[c + 12] + (catalogue[c + 14] << 5));
int number_of_records = catalogue[c + 15];
size_t required_size = extent * bytes_per_catalogue_entry + (size_t)number_of_records * 128;
if(result->files[index].data.size() < required_size) {
result->files[index].data.resize(required_size);
}
int sectors_per_block = parameters.block_size / (int)sector_size;
int records_per_sector = (int)sector_size / 128;
int record = 0;
for(size_t block = 0; block < (has_long_allocation_units ? 8 : 16) && record < number_of_records; block++) {
int block_number;
if(has_long_allocation_units) {
block_number = catalogue[c + 16 + (block << 1)] + (catalogue[c + 16 + (block << 1) + 1] << 8);
} else {
block_number = catalogue[c + 16 + block];
}
if(!block_number) {
record += parameters.block_size / 128;
continue;
}
int first_sector = block_number * sectors_per_block;
sector = first_sector % parameters.sectors_per_track;
track = first_sector / parameters.sectors_per_track;
for(int s = 0; s < sectors_per_block && record < number_of_records; s++) {
std::shared_ptr<Storage::Encodings::MFM::Sector> sector_contents = parser.get_sector(0, (uint8_t)track, (uint8_t)(parameters.first_sector + sector));
if(!sector_contents) break;
sector++;
if(sector == parameters.sectors_per_track) {
sector = 0;
track++;
}
int records_to_copy = std::min(number_of_records - record, records_per_sector);
memcpy(&result->files[index].data[extent * bytes_per_catalogue_entry + (size_t)record * 128], sector_contents->data.data(), (size_t)records_to_copy * 128);
record += records_to_copy;
}
}
}
return result;
}

View File

@ -0,0 +1,51 @@
//
// CPM.hpp
// Clock Signal
//
// Created by Thomas Harte on 10/08/2017.
// Copyright © 2017 Thomas Harte. All rights reserved.
//
#ifndef Storage_Disk_Parsers_CPM_hpp
#define Storage_Disk_Parsers_CPM_hpp
#include "../Disk.hpp"
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
namespace Storage {
namespace Disk {
namespace CPM {
struct ParameterBlock {
int sectors_per_track;
int tracks;
int block_size;
int first_sector;
uint16_t catalogue_allocation_bitmap;
int reserved_tracks;
};
struct File {
uint8_t user_number;
std::string name;
std::string type;
bool read_only;
bool system;
std::vector<uint8_t> data;
};
struct Catalogue {
std::vector<File> files;
};
std::unique_ptr<Catalogue> GetCatalogue(const std::shared_ptr<Storage::Disk::Disk> &disk, const ParameterBlock &parameters);
}
}
}
#endif /* Storage_Disk_Parsers_CPM_hpp */