1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-11-29 12:50:28 +00:00

Merge pull request #447 from TomHarte/DiskIIWriting

Substantially improves Disk II emulation, including write support
This commit is contained in:
Thomas Harte 2018-05-22 21:54:16 -04:00 committed by GitHub
commit d3c5e4267f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 430 additions and 151 deletions

View File

@ -25,6 +25,7 @@ DiskII::DiskII() :
{
drives_[0].set_sleep_observer(this);
drives_[1].set_sleep_observer(this);
drives_[active_drive_].set_event_delegate(this);
}
void DiskII::set_control(Control control, bool on) {
@ -36,14 +37,11 @@ void DiskII::set_control(Control control, bool on) {
case Control::P3: stepper_mask_ = (stepper_mask_ & 0x7) | (on ? 0x8 : 0x0); break;
case Control::Motor:
// TODO: does the motor control trigger both motors at once?
drives_[0].set_motor_on(on);
drives_[1].set_motor_on(on);
break;
motor_is_enabled_ = on;
drives_[active_drive_].set_motor_on(on);
return;
}
// printf("%0x: Set control %d %s\n", stepper_mask_, control, on ? "on" : "off");
// If the stepper magnet selections have changed, and any is on, see how
// that moves the head.
if(previous_stepper_mask ^ stepper_mask_ && stepper_mask_) {
@ -63,53 +61,53 @@ void DiskII::set_control(Control control, bool on) {
}
}
void DiskII::set_mode(Mode mode) {
// printf("Set mode %d\n", mode);
inputs_ = (inputs_ & ~input_mode) | ((mode == Mode::Write) ? input_mode : 0);
set_controller_can_sleep();
}
void DiskII::select_drive(int drive) {
// printf("Select drive %d\n", drive);
active_drive_ = drive & 1;
if((drive&1) == active_drive_) return;
drives_[active_drive_].set_event_delegate(this);
drives_[active_drive_^1].set_event_delegate(nullptr);
}
void DiskII::set_data_register(uint8_t value) {
// printf("Set data register (?)\n");
inputs_ |= input_command;
data_register_ = value;
set_controller_can_sleep();
}
uint8_t DiskII::get_shift_register() {
// if(shift_register_ & 0x80) printf("[%02x] ", shift_register_);
inputs_ &= ~input_command;
set_controller_can_sleep();
return shift_register_;
drives_[active_drive_].set_motor_on(false);
active_drive_ = drive & 1;
drives_[active_drive_].set_motor_on(motor_is_enabled_);
}
void DiskII::run_for(const Cycles cycles) {
if(is_sleeping()) return;
int integer_cycles = cycles.as_int();
if(!controller_can_sleep_) {
int integer_cycles = cycles.as_int();
while(integer_cycles--) {
const int address = (state_ & 0xf0) | inputs_ | ((shift_register_&0x80) >> 6);
inputs_ |= input_flux;
state_ = state_machine_[static_cast<std::size_t>(address)];
switch(state_ & 0xf) {
case 0x0: shift_register_ = 0; break; // clear
default: shift_register_ = 0; break; // clear
case 0x8: break; // nop
case 0x9: shift_register_ = static_cast<uint8_t>(shift_register_ << 1); break; // shift left, bringing in a zero
case 0xd: shift_register_ = static_cast<uint8_t>((shift_register_ << 1) | 1); break; // shift left, bringing in a one
case 0xb: shift_register_ = data_register_; break; // load
case 0xa: // 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,
// short circuit further work and return now.
if(shift_register_ == is_write_protected() ? 0xff : 0x00) {
if(!drive_is_sleeping_[0]) drives_[0].run_for(Cycles(integer_cycles));
if(!drive_is_sleeping_[1]) drives_[1].run_for(Cycles(integer_cycles));
set_controller_can_sleep();
return;
}
break;
default: break;
case 0xb: 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));
}
// TODO: surely there's a less heavyweight solution than this?
@ -127,14 +125,23 @@ void DiskII::run_for(const Cycles cycles) {
void DiskII::set_controller_can_sleep() {
// Permit the controller to sleep if it's in sense write protect mode, and the shift register
// has already filled with the result of shifting eight times.
bool controller_could_sleep = controller_can_sleep_;
controller_can_sleep_ =
(
(inputs_ == input_flux) &&
!motor_is_enabled_ &&
!shift_register_
) ||
(
(inputs_ == (input_command | input_flux)) &&
(shift_register_ == (is_write_protected() ? 0xff : 0x00));
if(is_sleeping()) update_sleep_observer();
(shift_register_ == (is_write_protected() ? 0xff : 0x00))
);
if(controller_could_sleep != controller_can_sleep_)
update_sleep_observer();
}
bool DiskII::is_write_protected() {
return true;
return !!(stepper_mask_ & 2) | drives_[active_drive_].get_is_read_only();
}
void DiskII::set_state_machine(const std::vector<uint8_t> &state_machine) {
@ -143,9 +150,9 @@ void DiskII::set_state_machine(const std::vector<uint8_t> &state_machine) {
state b0, state b2, state b3, pulse, Q7, Q6, shift, state b1
... and has the top nibble reflected. Beneath Apple Pro-DOS uses a
different order and several of the online copies are reformatted
into that order.
... and has the top nibble of each value stored in the ROM reflected.
Beneath Apple Pro-DOS uses a different order and several of the
online copies are reformatted into that order.
So the code below remaps into Beneath Apple Pro-DOS order if the
supplied state machine isn't already in that order.
@ -202,15 +209,11 @@ bool DiskII::is_sleeping() {
return controller_can_sleep_ && drive_is_sleeping_[0] && drive_is_sleeping_[1];
}
void DiskII::set_register(int address, uint8_t value) {
trigger_address(address, value);
void DiskII::set_data_input(uint8_t input) {
data_input_ = input;
}
uint8_t DiskII::get_register(int address) {
return trigger_address(address, 0xff);
}
uint8_t DiskII::trigger_address(int address, uint8_t value) {
int DiskII::read_address(int address) {
switch(address & 0xf) {
default:
case 0x0: set_control(Control::P0, false); break;
@ -222,19 +225,30 @@ uint8_t DiskII::trigger_address(int address, uint8_t value) {
case 0x6: set_control(Control::P3, false); break;
case 0x7: set_control(Control::P3, true); break;
case 0x8: set_control(Control::Motor, false); break;
case 0x8:
shift_register_ = 0;
set_control(Control::Motor, false);
break;
case 0x9: set_control(Control::Motor, true); break;
case 0xa: select_drive(0); break;
case 0xb: select_drive(1); break;
case 0xc: return get_shift_register();
case 0xd: set_data_register(value); break;
case 0xe: set_mode(Mode::Read); break;
case 0xf: set_mode(Mode::Write); break;
case 0xc: inputs_ &= ~input_command; break;
case 0xd: inputs_ |= input_command; break;
case 0xe:
if(inputs_ & input_mode)
drives_[active_drive_].end_writing();
inputs_ &= ~input_mode;
break;
case 0xf:
if(!(inputs_ & input_mode))
drives_[active_drive_].begin_writing(Storage::Time(1, 2045454), false);
inputs_ |= input_mode;
break;
}
return 0xff;
set_controller_can_sleep();
return (address & 1) ? 0xff : shift_register_;
}
void DiskII::set_activity_observer(Activity::Observer *observer) {

View File

@ -33,15 +33,52 @@ class DiskII:
public:
DiskII();
void set_register(int address, uint8_t value);
uint8_t get_register(int address);
/// Sets the current external value of the data bus.
void set_data_input(uint8_t input);
/*!
Submits an access to address @c address.
@returns The 8-bit value loaded to the data bus by the DiskII if any;
@c DidNotLoad otherwise.
*/
int read_address(int address);
/*!
The value returned by @c read_address if accessing that address
didn't cause the disk II to place anything onto the bus.
*/
const int DidNotLoad = -1;
/// Advances the controller by @c cycles.
void run_for(const Cycles cycles);
/*!
Supplies the image of the state machine (i.e. P6) ROM,
which dictates how the Disk II will respond to input.
To reduce processing costs, some assumptions are made by
the implementation as to the content of this ROM.
Including:
* If Q6 is set and Q7 is reset, the controller is testing
for write protect. If and when the shift register has
become full with the state of the write protect value,
no further processing is required.
* If both Q6 and Q7 are reset, the drive motor is disabled,
and the shift register is all zeroes, no further processing
is required.
*/
void set_state_machine(const std::vector<uint8_t> &);
/// Inserts @c disk into the drive @c drive.
void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive);
// As per Sleeper.
bool is_sleeping() override;
// The Disk II functions as a potential target for @c Activity::Sources.
void set_activity_observer(Activity::Observer *observer);
private:
@ -55,8 +92,6 @@ class DiskII:
void set_control(Control control, bool on);
void set_mode(Mode mode);
void select_drive(int drive);
void set_data_register(uint8_t value);
uint8_t get_shift_register();
uint8_t trigger_address(int address, uint8_t value);
void process_event(const Storage::Disk::Track::Event &event) override;
@ -65,7 +100,6 @@ class DiskII:
uint8_t state_ = 0;
uint8_t inputs_ = 0;
uint8_t shift_register_ = 0;
uint8_t data_register_ = 0;
int stepper_mask_ = 0;
int stepper_position_ = 0;
@ -76,8 +110,11 @@ class DiskII:
bool drive_is_sleeping_[2];
bool controller_can_sleep_ = false;
int active_drive_ = 0;
bool motor_is_enabled_ = false;
void set_controller_can_sleep();
uint8_t data_input_ = 0;
};
}

View File

@ -39,7 +39,9 @@ void BestEffortUpdater::update() {
const int64_t integer_duration = std::chrono::duration_cast<std::chrono::nanoseconds>(elapsed).count();
if(integer_duration > 0) {
if(delegate_) {
const double duration = static_cast<double>(integer_duration) / 1e9;
// Cap running at 1/5th of a second, to avoid doing a huge amount of work after any
// brief system interruption.
const double duration = std::min(static_cast<double>(integer_duration) / 1e9, 0.2);
delegate_->update(this, duration, has_skipped_);
}
has_skipped_ = false;

View File

@ -38,7 +38,8 @@ class ConcreteMachine:
public CPU::MOS6502::BusHandler,
public Inputs::Keyboard,
public AppleII::Machine,
public Activity::Source {
public Activity::Source,
public AppleII::Card::Delegate {
private:
struct VideoBusHandler : public AppleII::Video::BusHandler {
public:
@ -65,9 +66,9 @@ class ConcreteMachine:
void update_audio() {
speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(audio_divider)));
}
void update_cards() {
for(const auto &card : cards_) {
if(card) card->run_for(cycles_since_card_update_, stretched_cycles_since_card_update_);
void update_just_in_time_cards() {
for(const auto &card : just_in_time_cards_) {
card->run_for(cycles_since_card_update_, stretched_cycles_since_card_update_);
}
cycles_since_card_update_ = 0;
stretched_cycles_since_card_update_ = 0;
@ -84,10 +85,42 @@ class ConcreteMachine:
Cycles cycles_since_audio_update_;
ROMMachine::ROMFetcher rom_fetcher_;
// MARK: - Cards
std::array<std::unique_ptr<AppleII::Card>, 7> cards_;
Cycles cycles_since_card_update_;
std::vector<AppleII::Card *> every_cycle_cards_;
std::vector<AppleII::Card *> just_in_time_cards_;
int stretched_cycles_since_card_update_ = 0;
void install_card(std::size_t slot, AppleII::Card *card) {
assert(slot >= 1 && slot < 8);
cards_[slot - 1].reset(card);
card->set_delegate(this);
pick_card_messaging_group(card);
}
bool is_every_cycle_card(AppleII::Card *card) {
return !card->get_select_constraints();
}
void pick_card_messaging_group(AppleII::Card *card) {
const bool is_every_cycle = is_every_cycle_card(card);
std::vector<AppleII::Card *> &intended = is_every_cycle ? every_cycle_cards_ : just_in_time_cards_;
std::vector<AppleII::Card *> &undesired = is_every_cycle ? just_in_time_cards_ : every_cycle_cards_;
if(std::find(intended.begin(), intended.end(), card) != intended.end()) return;
auto old_membership = std::find(undesired.begin(), undesired.end(), card);
if(old_membership != undesired.end()) undesired.erase(old_membership);
intended.push_back(card);
}
void card_did_change_select_constraints(AppleII::Card *card) override {
pick_card_messaging_group(card);
}
// MARK: - Memory Map
struct MemoryBlock {
uint8_t *read_pointer = nullptr;
uint8_t *write_pointer = nullptr;
@ -174,6 +207,19 @@ class ConcreteMachine:
++ cycles_since_card_update_;
cycles_since_audio_update_ += Cycles(7);
// The Apple II has a slightly weird timing pattern: every 65th CPU cycle is stretched
// by an extra 1/7th. That's because one cycle lasts 3.5 NTSC colour clocks, so after
// 65 cycles a full line of 227.5 colour clocks have passed. But the high-rate binary
// signal approximation that produces colour needs to be in phase, so a stretch of exactly
// 0.5 further colour cycles is added. The video class handles that implicitly, but it
// needs to be accumulated here for the audio.
cycles_into_current_line_ = (cycles_into_current_line_ + 1) % 65;
const bool is_stretched_cycle = !cycles_into_current_line_;
if(is_stretched_cycle) {
++ cycles_since_audio_update_;
++ stretched_cycles_since_card_update_;
}
/*
There are five distinct zones of memory on an Apple II:
@ -192,8 +238,9 @@ class ConcreteMachine:
}
else if(address < 0xd000) block = nullptr;
else if(address < 0xe000) {block = &memory_blocks_[2]; address -= 0xd000; }
else {block = &memory_blocks_[3]; address -= 0xe000; }
else { block = &memory_blocks_[3]; address -= 0xe000; }
bool has_updated_cards = false;
if(block) {
if(isReadOperation(operation)) *value = block->read_pointer[address];
else if(block->write_pointer) block->write_pointer[address] = *value;
@ -286,39 +333,60 @@ class ConcreteMachine:
break;
}
if(address >= 0xc100 && address < 0xc800) {
/*
Communication with cards follows.
*/
if(address >= 0xc090 && address < 0xc800) {
// If this is a card access, figure out which card is at play before determining
// the totality of who needs messaging.
size_t card_number = 0;
AppleII::Card::Select select = AppleII::Card::None;
if(address >= 0xc100) {
/*
Decode the area conventionally used by cards for ROMs:
0xCn00 to 0xCnff: card n.
*/
const size_t card_number = (address - 0xc100) >> 8;
if(cards_[card_number]) {
update_cards();
cards_[card_number]->perform_bus_operation(operation, address & 0xff, value);
}
} else if(address >= 0xc090 && address < 0xc100) {
card_number = (address - 0xc100) >> 8;
select = AppleII::Card::Device;
} else {
/*
Decode the area conventionally used by cards for registers:
C0n0 to C0nF: card n - 8.
*/
const size_t card_number = (address - 0xc090) >> 4;
if(cards_[card_number]) {
update_cards();
cards_[card_number]->perform_bus_operation(operation, 0x100 | (address&0xf), value);
card_number = (address - 0xc090) >> 4;
select = AppleII::Card::IO;
}
// If the selected card is a just-in-time card, update the just-in-time cards,
// and then message it specifically.
const bool is_read = isReadOperation(operation);
AppleII::Card *const target = cards_[card_number].get();
if(target && !is_every_cycle_card(target)) {
update_just_in_time_cards();
target->perform_bus_operation(select, is_read, address, value);
}
// Update all the every-cycle cards regardless, but send them a ::None select if they're
// not the one actually selected.
for(const auto &card: every_cycle_cards_) {
card->run_for(Cycles(1), is_stretched_cycle);
card->perform_bus_operation(
(card == target) ? select : AppleII::Card::None,
is_read, address, value);
}
has_updated_cards = true;
}
}
// The Apple II has a slightly weird timing pattern: every 65th CPU cycle is stretched
// by an extra 1/7th. That's because one cycle lasts 3.5 NTSC colour clocks, so after
// 65 cycles a full line of 227.5 colour clocks have passed. But the high-rate binary
// signal approximation that produces colour needs to be in phase, so a stretch of exactly
// 0.5 further colour cycles is added. The video class handles that implicitly, but it
// needs to be accumulated here for the audio.
cycles_into_current_line_ = (cycles_into_current_line_ + 1) % 65;
if(!cycles_into_current_line_) {
++ cycles_since_audio_update_;
++ stretched_cycles_since_card_update_;
if(!has_updated_cards && !every_cycle_cards_.empty()) {
// Update all every-cycle cards and give them the cycle.
const bool is_read = isReadOperation(operation);
for(const auto &card: every_cycle_cards_) {
card->run_for(Cycles(1), is_stretched_cycle);
card->perform_bus_operation(AppleII::Card::None, is_read, address, value);
}
}
return Cycles(1);
@ -327,7 +395,7 @@ class ConcreteMachine:
void flush() {
update_video();
update_audio();
update_cards();
update_just_in_time_cards();
audio_queue_.perform();
}
@ -391,7 +459,8 @@ class ConcreteMachine:
auto *const apple_target = dynamic_cast<const Target *>(target);
if(apple_target->disk_controller != Target::DiskController::None) {
cards_[5].reset(new AppleII::DiskIICard(rom_fetcher_, apple_target->disk_controller == Target::DiskController::SixteenSector));
// Apple recommended slot 6 for the (first) Disk II.
install_card(6, new AppleII::DiskIICard(rom_fetcher_, apple_target->disk_controller == Target::DiskController::SixteenSector));
}
rom_ = (apple_target->model == Target::Model::II) ? apple2_rom_ : apple2plus_rom_;

View File

@ -15,16 +15,97 @@
namespace AppleII {
/*!
This provides a small subset of the interface offered to cards installed in
an Apple II, oriented pragmatically around the cards that are implemented.
The main underlying rule is as it is elsewhere in the emulator: no
_inaccurate_ simplifications no provision of information that shouldn't
actually commute, no interfaces that say they do one thing but which by both
both sides are coupled through an unwritten understanding of abuse.
Special notes:
Devices that announce a select constraint, being interested in acting only
when their IO or Device select is active will receive just-in-time @c run_for
notifications, as well as being updated at the end of each of the Apple's
@c run_for periods, prior to a @c flush.
Devices that do not announce a select constraint will prima facie receive a
@c perform_bus_operation every cycle. They'll also receive a @c flush.
It is **highly** recomended that such devices also implement @c Sleeper
as they otherwise prima facie require a virtual method call every
single cycle.
*/
class Card {
public:
/*! Advances time by @c cycles, of which @c stretches were stretched. */
enum Select: int {
None = 0, // No select line is active
IO = 1 << 0, // IO select is active
Device = 1 << 1, // Device select is active
};
/*!
Advances time by @c cycles, of which @c stretches were stretched.
This is posted only to cards that announced a select constraint. Cards with
no constraints, that want to be informed of every machine cycle, will receive
a call to perform_bus_operation every cycle and should use that for time keeping.
*/
virtual void run_for(Cycles half_cycles, int stretches) {}
/*! Performs a bus operation; the card is implicitly selected. */
virtual void perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value) = 0;
/// Requests a flush of any pending audio or video output.
virtual void flush() {}
/*! Supplies a target for observers. */
/*!
Performs a bus operation.
@param select The state of the card's select lines: indicates whether the Apple II
thinks this card should respond as though this were an IO access, a Device access,
or it thinks that the card shouldn't respond.
@param is_read @c true if this is a read cycle; @c false otherwise.
@param address The current value of the address bus.
@param value A pointer to the value of the data bus, not accounting input from cards.
If this is a read cycle, the card is permitted to replace this value with the value
output by the card, if any. If this is a write cycle, the card should only read
this value.
*/
virtual void perform_bus_operation(Select select, bool is_read, uint16_t address, uint8_t *value) = 0;
/*!
Returns the type of bus selects this card is actually interested in.
As specified, the default is that cards will ask to receive perform_bus_operation
only when their select lines are active.
There's a substantial caveat here: cards that register to receive @c None
will receive a perform_bus_operation every cycle. To reduce the number of
virtual method calls, they **will not** receive run_for. run_for will propagate
only to cards that register for IO and/or Device accesses only.
*/
int get_select_constraints() {
return select_constraints_;
}
/*! Cards may supply a target for activity observation if desired. */
virtual void set_activity_observer(Activity::Observer *observer) {}
struct Delegate {
virtual void card_did_change_select_constraints(Card *card) = 0;
};
void set_delegate(Delegate *delegate) {
delegate_ = delegate;
}
protected:
int select_constraints_ = IO | Device;
Delegate *delegate_ = nullptr;
void set_select_constraints(int constraints) {
if(constraints == select_constraints_) return;
select_constraints_ = constraints;
if(delegate_) delegate_->card_did_change_select_constraints(this);
}
};
}

View File

@ -19,21 +19,29 @@ DiskIICard::DiskIICard(const ROMMachine::ROMFetcher &rom_fetcher, bool is_16_sec
});
boot_ = std::move(*roms[0]);
diskii_.set_state_machine(*roms[1]);
set_select_constraints(None);
diskii_.set_sleep_observer(this);
}
void DiskIICard::perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value) {
if(address < 0x100) {
if(isReadOperation(operation)) *value = boot_[address];
} else {
if(isReadOperation(operation)) {
*value = diskii_.get_register(address);
} else {
diskii_.set_register(address, *value);
void DiskIICard::perform_bus_operation(Select select, bool is_read, uint16_t address, uint8_t *value) {
diskii_.set_data_input(*value);
switch(select) {
default: break;
case IO: {
const int disk_value = diskii_.read_address(address);
if(is_read) {
if(disk_value != diskii_.DidNotLoad)
*value = static_cast<uint8_t>(disk_value);
}
} break;
case Device:
if(is_read) *value = boot_[address & 0xff];
break;
}
}
void DiskIICard::run_for(Cycles cycles, int stretches) {
if(diskii_is_sleeping_) return;
diskii_.run_for(Cycles(cycles.as_int() * 2));
}
@ -44,3 +52,8 @@ void DiskIICard::set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int
void DiskIICard::set_activity_observer(Activity::Observer *observer) {
diskii_.set_activity_observer(observer);
}
void DiskIICard::set_component_is_sleeping(Sleeper *component, bool is_sleeping) {
diskii_is_sleeping_ = is_sleeping;
set_select_constraints(is_sleeping ? (IO | Device) : 0);
}

View File

@ -14,6 +14,7 @@
#include "../../Components/DiskII/DiskII.hpp"
#include "../../Storage/Disk/Disk.hpp"
#include "../../ClockReceiver/Sleeper.hpp"
#include <cstdint>
#include <memory>
@ -21,19 +22,22 @@
namespace AppleII {
class DiskIICard: public Card {
class DiskIICard: public Card, public Sleeper::SleepObserver {
public:
DiskIICard(const ROMMachine::ROMFetcher &rom_fetcher, bool is_16_sector);
void perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value) override;
void perform_bus_operation(Select select, bool is_read, uint16_t address, uint8_t *value) override;
void run_for(Cycles cycles, int stretches) override;
void set_activity_observer(Activity::Observer *observer) override;
void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive);
private:
void set_component_is_sleeping(Sleeper *component, bool is_sleeping) override;
std::vector<uint8_t> boot_;
Apple::DiskII diskii_;
bool diskii_is_sleeping_ = false;
};
}

View File

@ -201,6 +201,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
public Utility::TypeRecipient,
public Storage::Tape::BinaryTapePlayer::Delegate,
public Microdisc::Delegate,
public Sleeper::SleepObserver,
public Activity::Source,
public Machine {
@ -216,6 +217,10 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
via_port_handler_.set_interrupt_delegate(this);
tape_player_.set_delegate(this);
Memory::Fuzz(ram_, sizeof(ram_));
if(disk_interface == Analyser::Static::Oric::Target::DiskInterface::Pravetz) {
diskii_.set_sleep_observer(this);
}
}
~ConcreteMachine() {
@ -405,9 +410,8 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
}
}
} else {
update_diskii();
if(isReadOperation(operation)) *value = diskii_.get_register(address);
else diskii_.set_register(address, *value);
const int disk_value = diskii_.read_address(address);
if(isReadOperation(operation) && disk_value != diskii_.DidNotLoad) *value = static_cast<uint8_t>(disk_value);
}
break;
}
@ -436,8 +440,15 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
tape_player_.run_for(Cycles(1));
switch(disk_interface) {
default: break;
case Analyser::Static::Oric::Target::DiskInterface::Microdisc: microdisc_.run_for(Cycles(8)); break;
case Analyser::Static::Oric::Target::DiskInterface::Pravetz: cycles_since_diskii_update_ += 2; break;
case Analyser::Static::Oric::Target::DiskInterface::Microdisc:
microdisc_.run_for(Cycles(8));
break;
case Analyser::Static::Oric::Target::DiskInterface::Pravetz:
if(!diskii_is_sleeping_) {
diskii_.set_data_input(*value);
diskii_.run_for(Cycles(2));
}
break;
}
cycles_since_video_update_++;
return Cycles(1);
@ -446,7 +457,6 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
forceinline void flush() {
update_video();
via_port_handler_.flush();
if(disk_interface == Analyser::Static::Oric::Target::DiskInterface::Pravetz) update_diskii();
}
// to satisfy CRTMachine::Machine
@ -561,6 +571,10 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
}
}
void set_component_is_sleeping(Sleeper *component, bool is_sleeping) override final {
diskii_is_sleeping_ = diskii_.is_sleeping();
}
private:
const uint16_t basic_invisible_ram_top_ = 0xffff;
const uint16_t basic_visible_ram_top_ = 0xbfff;
@ -605,10 +619,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
Apple::DiskII diskii_;
std::vector<uint8_t> pravetz_rom_;
std::size_t pravetz_rom_base_pointer_ = 0;
Cycles cycles_since_diskii_update_;
void update_diskii() {
diskii_.run_for(cycles_since_diskii_update_.flush());
}
bool diskii_is_sleeping_ = false;
// Overlay RAM
uint16_t ram_top_ = basic_visible_ram_top_;

View File

@ -9,7 +9,9 @@
#include "AppleDSK.hpp"
#include "../../Track/PCMTrack.hpp"
#include "../../Track/TrackSerialiser.hpp"
#include "../../Encodings/AppleGCR/Encoder.hpp"
#include "../../Encodings/AppleGCR/SegmentParser.hpp"
using namespace Storage::Disk;
@ -42,10 +44,28 @@ HeadPosition AppleDSK::get_maximum_head_position() {
return HeadPosition(number_of_tracks);
}
bool AppleDSK::get_is_read_only() {
return file_.get_is_known_read_only();
}
long AppleDSK::file_offset(Track::Address address) {
return address.position.as_int() * bytes_per_sector * sectors_per_track_;
}
size_t AppleDSK::logical_sector_for_physical_sector(size_t physical) {
// DOS and Pro DOS interleave sectors on disk, and they're represented in a disk
// image in physical order rather than logical.
if(physical == 15) return 15;
return (physical * (is_prodos_ ? 8 : 7)) % 15;
}
std::shared_ptr<Track> AppleDSK::get_track_at_position(Track::Address address) {
const long file_offset = address.position.as_int() * bytes_per_sector * sectors_per_track_;
file_.seek(file_offset, SEEK_SET);
const std::vector<uint8_t> track_data = file_.read(static_cast<size_t>(bytes_per_sector * sectors_per_track_));
std::vector<uint8_t> track_data;
{
std::lock_guard<std::mutex> lock_guard(file_.get_file_access_mutex());
file_.seek(file_offset(address), SEEK_SET);
track_data = file_.read(static_cast<size_t>(bytes_per_sector * sectors_per_track_));
}
Storage::Disk::PCMSegment segment;
const uint8_t track = static_cast<uint8_t>(address.position.as_int());
@ -53,17 +73,11 @@ std::shared_ptr<Track> AppleDSK::get_track_at_position(Track::Address address) {
// In either case below, the code aims for exactly 50,000 bits per track.
if(sectors_per_track_ == 16) {
// Write the sectors.
std::size_t sector_number_ = 0;
for(uint8_t c = 0; c < 16; ++c) {
segment += Encodings::AppleGCR::six_and_two_sync(10);
segment += Encodings::AppleGCR::header(254, track, c);
segment += Encodings::AppleGCR::six_and_two_sync(10);
segment += Encodings::AppleGCR::six_and_two_data(&track_data[sector_number_ * 256]);
// DOS and Pro DOS interleave sectors on disk, and they're represented in a disk
// image in physical order rather than logical. So that skew needs to be applied here.
sector_number_ += is_prodos_ ? 8 : 7;
if(sector_number_ > 0xf) sector_number_ %= 15;
segment += Encodings::AppleGCR::six_and_two_data(&track_data[logical_sector_for_physical_sector(c) * 256]);
}
// Pad if necessary.
@ -71,8 +85,33 @@ std::shared_ptr<Track> AppleDSK::get_track_at_position(Track::Address address) {
segment += Encodings::AppleGCR::six_and_two_sync((50000 - segment.number_of_bits) / 10);
}
} else {
// TODO: 5 and 3, 13-sector format. If DSK actually supports it?
}
return std::make_shared<PCMTrack>(segment);
}
void AppleDSK::set_tracks(const std::map<Track::Address, std::shared_ptr<Track>> &tracks) {
std::map<Track::Address, std::vector<uint8_t>> tracks_by_address;
for(const auto &pair: tracks) {
// Decode the track.
auto sector_map = Storage::Encodings::AppleGCR::sectors_from_segment(
Storage::Disk::track_serialisation(*pair.second, Storage::Time(1, 50000)));
// Rearrange sectors into Apple DOS or Pro-DOS order.
std::vector<uint8_t> track_contents(static_cast<size_t>(bytes_per_sector * sectors_per_track_));
for(const auto &sector_pair: sector_map) {
const size_t target_address = logical_sector_for_physical_sector(sector_pair.second.address.sector);
memcpy(&track_contents[target_address*256], sector_pair.second.data.data(), bytes_per_sector);
}
// Store for later.
tracks_by_address[pair.first] = std::move(track_contents);
}
std::lock_guard<std::mutex> lock_guard(file_.get_file_access_mutex());
for(const auto &pair: tracks_by_address) {
file_.seek(file_offset(pair.first), SEEK_SET);
file_.write(pair.second);
}
}

View File

@ -31,14 +31,19 @@ class AppleDSK: public DiskImage {
*/
AppleDSK(const std::string &file_name);
// implemented to satisfy @c Disk
// Implemented to satisfy @c Disk.
HeadPosition get_maximum_head_position() override;
std::shared_ptr<Track> get_track_at_position(Track::Address address) override;
void set_tracks(const std::map<Track::Address, std::shared_ptr<Track>> &tracks) override;
bool get_is_read_only() override;
private:
Storage::FileHolder file_;
int sectors_per_track_ = 16;
bool is_prodos_ = false;
long file_offset(Track::Address address);
size_t logical_sector_for_physical_sector(size_t physical);
};
}

View File

@ -27,7 +27,7 @@ PCMPatchedTrack::PCMPatchedTrack(const PCMPatchedTrack &original) {
active_period_ = periods_.begin();
}
Track *PCMPatchedTrack::clone() {
Track *PCMPatchedTrack::clone() const {
return new PCMPatchedTrack(*this);
}
@ -202,7 +202,8 @@ Storage::Time PCMPatchedTrack::seek_to(const Time &time_since_index_hole) {
else
current_time_ = underlying_track_->seek_to(time_since_index_hole);
assert(current_time_ <= time_since_index_hole);
// The assert below is disabled as it assumes too much about total precision.
// assert(current_time_ <= time_since_index_hole);
return current_time_;
}

View File

@ -43,9 +43,9 @@ class PCMPatchedTrack: public Track {
void add_segment(const Time &start_time, const PCMSegment &segment, bool clamp_to_index_hole);
// To satisfy Storage::Disk::Track
Event get_next_event();
Time seek_to(const Time &time_since_index_hole);
Track *clone();
Event get_next_event() override;
Time seek_to(const Time &time_since_index_hole) override;
Track *clone() const override;
private:
std::shared_ptr<Track> underlying_track_;

View File

@ -46,7 +46,7 @@ PCMTrack::PCMTrack(const PCMTrack &original) : PCMTrack() {
segment_event_sources_ = original.segment_event_sources_;
}
Track *PCMTrack::clone() {
Track *PCMTrack::clone() const {
return new PCMTrack(*this);
}

View File

@ -42,9 +42,9 @@ class PCMTrack: public Track {
PCMTrack(const PCMTrack &);
// as per @c Track
Event get_next_event();
Time seek_to(const Time &time_since_index_hole);
Track *clone();
Event get_next_event() override;
Time seek_to(const Time &time_since_index_hole) override;
Track *clone() const override;
private:
// storage for the segments that describe this track

View File

@ -114,7 +114,7 @@ class Track {
/*!
The virtual copy constructor pattern; returns a copy of the Track.
*/
virtual Track *clone() = 0;
virtual Track *clone() const = 0;
};
}

View File

@ -8,11 +8,14 @@
#include "TrackSerialiser.hpp"
#include <memory>
// TODO: if this is a PCMTrack with only one segment and that segment's bit rate is within tolerance,
// just return a copy of that segment.
Storage::Disk::PCMSegment Storage::Disk::track_serialisation(Track &track, Time length_of_a_bit) {
Storage::Disk::PCMSegment Storage::Disk::track_serialisation(const Track &track, Time length_of_a_bit) {
unsigned int history_size = 16;
DigitalPhaseLockedLoop pll(100, history_size);
std::unique_ptr<Track> track_copy(track.clone());
struct ResultAccumulator: public DigitalPhaseLockedLoop::Delegate {
PCMSegment result;
@ -29,25 +32,25 @@ Storage::Disk::PCMSegment Storage::Disk::track_serialisation(Track &track, Time
length_multiplier.simplify();
// start at the index hole
track.seek_to(Time(0));
track_copy->seek_to(Time(0));
// grab events until the next index hole
Time time_error = Time(0);
while(true) {
Track::Event next_event = track.get_next_event();
Track::Event next_event = track_copy->get_next_event();
if(next_event.type == Track::Event::IndexHole) break;
Time extended_length = next_event.length * length_multiplier + time_error;
time_error.clock_rate = extended_length.clock_rate;
time_error.length = extended_length.length % extended_length.clock_rate;
pll.run_for(Cycles(extended_length.get<int>()));
pll.run_for(Cycles(static_cast<int>(extended_length.get<int64_t>())));
pll.add_pulse();
// If the PLL is now sufficiently primed, restart, and start recording bits this time.
if(history_size) {
history_size--;
if(!history_size) {
track.seek_to(Time(0));
track_copy->seek_to(Time(0));
time_error.set_zero();
pll.set_delegate(&result_accumulator);
}

View File

@ -30,7 +30,7 @@ namespace Disk {
@param length_of_a_bit The expected length of a single bit, as a proportion of the
track length.
*/
PCMSegment track_serialisation(Track &track, Time length_of_a_bit);
PCMSegment track_serialisation(const Track &track, Time length_of_a_bit);
}
}

View File

@ -21,6 +21,6 @@ Storage::Time UnformattedTrack::seek_to(const Time &time_since_index_hole) {
return Time(0);
}
Track *UnformattedTrack::clone() {
Track *UnformattedTrack::clone() const {
return new UnformattedTrack;
}

View File

@ -19,9 +19,9 @@ namespace Disk {
*/
class UnformattedTrack: public Track {
public:
Event get_next_event();
Time seek_to(const Time &time_since_index_hole);
Track *clone();
Event get_next_event() override;
Time seek_to(const Time &time_since_index_hole) override;
Track *clone() const override;
};
}