mirror of
https://github.com/TomHarte/CLK.git
synced 2025-10-25 09:27:01 +00:00
Compare commits
85 Commits
2018-07-11
...
2018-08-05
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2d9e1ec81 | ||
|
|
673b915ee8 | ||
|
|
032a62dfff | ||
|
|
f2d78182a3 | ||
|
|
de68e70246 | ||
|
|
e07447eb9a | ||
|
|
5cdeb58571 | ||
|
|
ce14cc8677 | ||
|
|
f7ce86fef8 | ||
|
|
55f2fccf5e | ||
|
|
101fb5d7bf | ||
|
|
3c51e335c3 | ||
|
|
33ea90678c | ||
|
|
11ae2c64ba | ||
|
|
26624d7652 | ||
|
|
85fb4773b0 | ||
|
|
099d66804e | ||
|
|
086596c28e | ||
|
|
3aeb4213fe | ||
|
|
558b96bc05 | ||
|
|
e97cc40a2c | ||
|
|
94503ed771 | ||
|
|
c4f86cc324 | ||
|
|
70c4d6b9b3 | ||
|
|
78c7137427 | ||
|
|
74a2f717b3 | ||
|
|
98bb5bd9f1 | ||
|
|
c91eaaf8da | ||
|
|
a36f37d240 | ||
|
|
c773d3501a | ||
|
|
5810f9b3f9 | ||
|
|
3f56683342 | ||
|
|
16ccbdefd6 | ||
|
|
a533d09fe7 | ||
|
|
e9aaa5bbdf | ||
|
|
ecb26e3281 | ||
|
|
5aa0b17720 | ||
|
|
632b37ecec | ||
|
|
c905de2e40 | ||
|
|
bc2afe69e1 | ||
|
|
894998b163 | ||
|
|
51192d8397 | ||
|
|
3c33ccd730 | ||
|
|
3e35109d63 | ||
|
|
99c770eab4 | ||
|
|
34aa78b7ce | ||
|
|
8cca9c2055 | ||
|
|
85ce21c79f | ||
|
|
d19d949b9c | ||
|
|
1cb3713b84 | ||
|
|
689850d698 | ||
|
|
c572a52049 | ||
|
|
41765e00c4 | ||
|
|
080aa0acc5 | ||
|
|
5e7c46a72a | ||
|
|
5f2b9b2d5a | ||
|
|
5c4506a9db | ||
|
|
55a6431fb3 | ||
|
|
ede2696a77 | ||
|
|
59b9e39022 | ||
|
|
6b2970f2f2 | ||
|
|
6a73fe7d65 | ||
|
|
1362906f94 | ||
|
|
8f4042c4bb | ||
|
|
c05b6397b0 | ||
|
|
8d18808efe | ||
|
|
09950d9414 | ||
|
|
badbbdf155 | ||
|
|
2832792fed | ||
|
|
efa45b9504 | ||
|
|
523749edf8 | ||
|
|
5a0499e8a7 | ||
|
|
258c8b5900 | ||
|
|
24b861f056 | ||
|
|
29f7f4d432 | ||
|
|
21080a1149 | ||
|
|
1d068fd09b | ||
|
|
92065813ef | ||
|
|
3e9ef6b8cb | ||
|
|
c9451a5382 | ||
|
|
2be3b027db | ||
|
|
e339d169c5 | ||
|
|
87001f86ee | ||
|
|
58484e8f37 | ||
|
|
00cb4d26b3 |
6
.editorconfig
Normal file
6
.editorconfig
Normal file
@@ -0,0 +1,6 @@
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
@@ -24,13 +24,13 @@ namespace Activity {
|
||||
class Observer {
|
||||
public:
|
||||
/// Announces to the receiver that there is an LED of name @c name.
|
||||
virtual void register_led(const std::string &name) = 0;
|
||||
virtual void register_led(const std::string &name) {}
|
||||
|
||||
/// Announces to the receiver that there is a drive of name @c name.
|
||||
virtual void register_drive(const std::string &name) = 0;
|
||||
virtual void register_drive(const std::string &name) {}
|
||||
|
||||
/// Informs the receiver of the new state of the LED with name @c name.
|
||||
virtual void set_led_status(const std::string &name, bool lit) = 0;
|
||||
virtual void set_led_status(const std::string &name, bool lit) {}
|
||||
|
||||
enum class DriveEvent {
|
||||
StepNormal,
|
||||
@@ -39,11 +39,10 @@ class Observer {
|
||||
};
|
||||
|
||||
/// Informs the receiver that the named event just occurred for the drive with name @c name.
|
||||
virtual void announce_drive_event(const std::string &name, DriveEvent event) = 0;
|
||||
virtual void announce_drive_event(const std::string &name, DriveEvent event) {}
|
||||
|
||||
/// Informs the receiver of the motor-on status of the drive with name @c name.
|
||||
virtual void set_drive_motor_status(const std::string &name, bool is_on) = 0;
|
||||
|
||||
virtual void set_drive_motor_status(const std::string &name, bool is_on) {}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ namespace AppleII {
|
||||
struct Target: public ::Analyser::Static::Target {
|
||||
enum class Model {
|
||||
II,
|
||||
IIplus
|
||||
IIplus,
|
||||
IIe
|
||||
};
|
||||
enum class DiskController {
|
||||
None,
|
||||
@@ -26,7 +27,7 @@ struct Target: public ::Analyser::Static::Target {
|
||||
ThirteenSector
|
||||
};
|
||||
|
||||
Model model = Model::IIplus;
|
||||
Model model = Model::IIe;
|
||||
DiskController disk_controller = DiskController::None;
|
||||
};
|
||||
|
||||
|
||||
@@ -17,13 +17,8 @@ static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>>
|
||||
|
||||
// only one mapped item is allowed
|
||||
if(segments.size() != 1) continue;
|
||||
|
||||
// which must be 8, 12, 16, 24 or 32 kb in size
|
||||
const Storage::Cartridge::Cartridge::Segment &segment = segments.front();
|
||||
const std::size_t data_size = segment.data.size();
|
||||
const std::size_t overflow = data_size&8191;
|
||||
if(overflow > 8 && overflow != 512 && (data_size != 12*1024)) continue;
|
||||
if(data_size < 8192) continue;
|
||||
|
||||
// the two bytes that will be first must be 0xaa and 0x55, either way around
|
||||
auto *start = &segment.data[0];
|
||||
@@ -34,19 +29,24 @@ static std::vector<std::shared_ptr<Storage::Cartridge::Cartridge>>
|
||||
if(start[0] == start[1]) continue;
|
||||
|
||||
// probability of a random binary blob that isn't a Coleco ROM proceeding to here is 1 - 1/32768.
|
||||
if(!overflow) {
|
||||
coleco_cartridges.push_back(cartridge);
|
||||
|
||||
// Round up to the next multiple of 8kb if this image is less than 32kb. Otherwise round down if
|
||||
// this image is within a short distance of 32kb.
|
||||
std::vector<Storage::Cartridge::Cartridge::Segment> output_segments;
|
||||
|
||||
size_t target_size;
|
||||
if(data_size >= 32*1024 && data_size < 32*1024 + 512) {
|
||||
target_size = 32 * 1024;
|
||||
} else {
|
||||
// Size down to a multiple of 8kb and apply the start address.
|
||||
std::vector<Storage::Cartridge::Cartridge::Segment> output_segments;
|
||||
|
||||
std::vector<uint8_t> truncated_data;
|
||||
std::vector<uint8_t>::difference_type truncated_size = static_cast<std::vector<uint8_t>::difference_type>(segment.data.size()) & ~8191;
|
||||
truncated_data.insert(truncated_data.begin(), segment.data.begin(), segment.data.begin() + truncated_size);
|
||||
output_segments.emplace_back(0x8000, truncated_data);
|
||||
|
||||
coleco_cartridges.emplace_back(new Storage::Cartridge::Cartridge(output_segments));
|
||||
target_size = data_size + ((8192 - (data_size & 8191)) & 8191);
|
||||
}
|
||||
|
||||
std::vector<uint8_t> truncated_data;
|
||||
truncated_data = segment.data;
|
||||
truncated_data.resize(target_size);
|
||||
output_segments.emplace_back(0x8000, truncated_data);
|
||||
|
||||
coleco_cartridges.emplace_back(new Storage::Cartridge::Cartridge(output_segments));
|
||||
}
|
||||
|
||||
return coleco_cartridges;
|
||||
|
||||
@@ -496,17 +496,25 @@ void TMS9918::run_for(const HalfCycles cycles) {
|
||||
}
|
||||
}
|
||||
|
||||
// Paint sprites and check for collisions.
|
||||
// Paint sprites and check for collisions, but only if at least one sprite is active
|
||||
// on this line.
|
||||
if(sprite_set.active_sprite_slot) {
|
||||
int sprite_pixels_left = pixels_left;
|
||||
const int shift_advance = sprites_magnified_ ? 1 : 2;
|
||||
|
||||
const uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff};
|
||||
const int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
|
||||
static const uint32_t sprite_colour_selection_masks[2] = {0x00000000, 0xffffffff};
|
||||
static const int colour_masks[16] = {0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
|
||||
|
||||
while(sprite_pixels_left--) {
|
||||
// sprite_colour is the colour that's going to reach the display after sprite logic has been
|
||||
// applied; by default assume that nothing is going to be drawn.
|
||||
uint32_t sprite_colour = pixel_base_[output_column_ - first_pixel_column_];
|
||||
|
||||
// The sprite_mask is used to keep track of whether two sprites have both sought to output
|
||||
// a pixel at the same location, and to feed that into the status register's sprite
|
||||
// collision bit.
|
||||
int sprite_mask = 0;
|
||||
|
||||
int c = sprite_set.active_sprite_slot;
|
||||
while(c--) {
|
||||
SpriteSet::ActiveSprite &sprite = sprite_set.active_sprites[c];
|
||||
@@ -517,15 +525,24 @@ void TMS9918::run_for(const HalfCycles cycles) {
|
||||
} else if(sprite.shift_position < 32) {
|
||||
int mask = sprite.image[sprite.shift_position >> 4] << ((sprite.shift_position&15) >> 1);
|
||||
mask = (mask >> 7) & 1;
|
||||
status_ |= (mask & sprite_mask) << StatusSpriteCollisionShift;
|
||||
sprite_mask |= mask;
|
||||
sprite.shift_position += shift_advance;
|
||||
|
||||
mask &= colour_masks[sprite.info[3]&15];
|
||||
sprite_colour = (sprite_colour & sprite_colour_selection_masks[mask^1]) | (palette[sprite.info[3]&15] & sprite_colour_selection_masks[mask]);
|
||||
// Ignore the right half of whatever was collected if sprites are not in 16x16 mode.
|
||||
if(sprite.shift_position < (sprites_16x16_ ? 32 : 16)) {
|
||||
// If any previous sprite has been painted in this column and this sprite
|
||||
// has this pixel set, set the sprite collision status bit.
|
||||
status_ |= (mask & sprite_mask) << StatusSpriteCollisionShift;
|
||||
sprite_mask |= mask;
|
||||
|
||||
// Check that the sprite colour is not transparent
|
||||
mask &= colour_masks[sprite.info[3]&15];
|
||||
sprite_colour = (sprite_colour & sprite_colour_selection_masks[mask^1]) | (palette[sprite.info[3]&15] & sprite_colour_selection_masks[mask]);
|
||||
}
|
||||
|
||||
sprite.shift_position += shift_advance;
|
||||
}
|
||||
}
|
||||
|
||||
// Output whichever sprite colour was on top.
|
||||
pixel_base_[output_column_ - first_pixel_column_] = sprite_colour;
|
||||
output_column_++;
|
||||
}
|
||||
|
||||
@@ -73,13 +73,18 @@ void DiskII::select_drive(int drive) {
|
||||
drives_[active_drive_].set_motor_on(motor_is_enabled_);
|
||||
}
|
||||
|
||||
// The read pulse is controlled by a special IC that outputs a 1us pulse for every field reversal on the disk.
|
||||
|
||||
void DiskII::run_for(const Cycles cycles) {
|
||||
if(preferred_clocking() == ClockingHint::Preference::None) return;
|
||||
|
||||
int integer_cycles = cycles.as_int();
|
||||
while(integer_cycles--) {
|
||||
const int address = (state_ & 0xf0) | inputs_ | ((shift_register_&0x80) >> 6);
|
||||
inputs_ |= input_flux;
|
||||
if(flux_duration_) {
|
||||
--flux_duration_;
|
||||
if(!flux_duration_) inputs_ |= input_flux;
|
||||
}
|
||||
state_ = state_machine_[static_cast<std::size_t>(address)];
|
||||
switch(state_ & 0xf) {
|
||||
default: shift_register_ = 0; break; // clear
|
||||
@@ -115,6 +120,15 @@ void DiskII::run_for(const Cycles cycles) {
|
||||
if(!drive_is_sleeping_[1]) drives_[1].run_for(Cycles(1));
|
||||
}
|
||||
|
||||
// Per comp.sys.apple2.programmer there is a delay between the controller
|
||||
// motor switch being flipped and the drive motor actually switching off.
|
||||
// This models that, accepting overrun as a risk.
|
||||
if(motor_off_time_ >= 0) {
|
||||
motor_off_time_ -= cycles.as_int();
|
||||
if(motor_off_time_ < 0) {
|
||||
set_control(Control::Motor, false);
|
||||
}
|
||||
}
|
||||
decide_clocking_preference();
|
||||
}
|
||||
|
||||
@@ -200,6 +214,7 @@ void DiskII::set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int driv
|
||||
void DiskII::process_event(const Storage::Disk::Track::Event &event) {
|
||||
if(event.type == Storage::Disk::Track::Event::FluxTransition) {
|
||||
inputs_ &= ~input_flux;
|
||||
flux_duration_ = 2; // Upon detection of a flux transition, the flux flag should stay set for 1us. Emulate that as two cycles.
|
||||
decide_clocking_preference();
|
||||
}
|
||||
}
|
||||
@@ -232,9 +247,12 @@ int DiskII::read_address(int address) {
|
||||
|
||||
case 0x8:
|
||||
shift_register_ = 0;
|
||||
set_control(Control::Motor, false);
|
||||
motor_off_time_ = clock_rate_;
|
||||
break;
|
||||
case 0x9:
|
||||
set_control(Control::Motor, true);
|
||||
motor_off_time_ = -1;
|
||||
break;
|
||||
case 0x9: set_control(Control::Motor, true); break;
|
||||
|
||||
case 0xa: select_drive(0); break;
|
||||
case 0xb: select_drive(1); break;
|
||||
|
||||
@@ -109,6 +109,7 @@ class DiskII:
|
||||
|
||||
int stepper_mask_ = 0;
|
||||
int stepper_position_ = 0;
|
||||
int motor_off_time_ = -1;
|
||||
|
||||
bool is_write_protected();
|
||||
std::array<uint8_t, 256> state_machine_;
|
||||
@@ -121,6 +122,7 @@ class DiskII:
|
||||
ClockingHint::Preference clocking_preference_ = ClockingHint::Preference::RealTime;
|
||||
|
||||
uint8_t data_input_ = 0;
|
||||
int flux_duration_ = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ struct BooleanSelection: public Selection {
|
||||
|
||||
struct ListSelection: public Selection {
|
||||
std::string value;
|
||||
|
||||
|
||||
ListSelection *list_selection();
|
||||
BooleanSelection *boolean_selection();
|
||||
ListSelection(const std::string value) : value(value) {}
|
||||
|
||||
@@ -128,6 +128,24 @@ class Joystick {
|
||||
set_input(input, 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
Gets the number of input fire buttons.
|
||||
|
||||
This is cached by default, but it's virtual so overridable.
|
||||
*/
|
||||
virtual int get_number_of_fire_buttons() {
|
||||
if(number_of_buttons_ >= 0) return number_of_buttons_;
|
||||
|
||||
number_of_buttons_ = 0;
|
||||
for(const auto &input: get_inputs()) {
|
||||
if(input.type == Input::Type::Fire) ++number_of_buttons_;
|
||||
}
|
||||
return number_of_buttons_;
|
||||
}
|
||||
|
||||
private:
|
||||
int number_of_buttons_ = -1;
|
||||
};
|
||||
|
||||
/*!
|
||||
@@ -168,10 +186,10 @@ class ConcreteJoystick: public Joystick {
|
||||
using Type = Joystick::Input::Type;
|
||||
switch(input.type) {
|
||||
default: did_set_input(input, is_active ? 1.0f : 0.0f); break;
|
||||
case Type::Left: did_set_input(Input(Type::Horizontal, input.info.control.index), is_active ? 0.25f : 0.5f); break;
|
||||
case Type::Right: did_set_input(Input(Type::Horizontal, input.info.control.index), is_active ? 0.75f : 0.5f); break;
|
||||
case Type::Up: did_set_input(Input(Type::Vertical, input.info.control.index), is_active ? 0.25f : 0.5f); break;
|
||||
case Type::Down: did_set_input(Input(Type::Vertical, input.info.control.index), is_active ? 0.75f : 0.5f); break;
|
||||
case Type::Left: did_set_input(Input(Type::Horizontal, input.info.control.index), is_active ? 0.1f : 0.5f); break;
|
||||
case Type::Right: did_set_input(Input(Type::Horizontal, input.info.control.index), is_active ? 0.9f : 0.5f); break;
|
||||
case Type::Up: did_set_input(Input(Type::Vertical, input.info.control.index), is_active ? 0.1f : 0.5f); break;
|
||||
case Type::Down: did_set_input(Input(Type::Vertical, input.info.control.index), is_active ? 0.9f : 0.5f); break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,27 +27,17 @@
|
||||
|
||||
#include "../../Analyser/Static/AppleII/Target.hpp"
|
||||
#include "../../ClockReceiver/ForceInline.hpp"
|
||||
#include "../../Configurable/Configurable.hpp"
|
||||
#include "../../Storage/Disk/Track/TrackSerialiser.hpp"
|
||||
#include "../../Storage/Disk/Encodings/AppleGCR/SegmentParser.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
std::vector<std::unique_ptr<Configurable::Option>> AppleII::get_options() {
|
||||
std::vector<std::unique_ptr<Configurable::Option>> options;
|
||||
options.emplace_back(new Configurable::BooleanOption("Accelerate DOS 3.3", "quickload"));
|
||||
return options;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
class ConcreteMachine:
|
||||
template <bool is_iie> class ConcreteMachine:
|
||||
public CRTMachine::Machine,
|
||||
public MediaTarget::Machine,
|
||||
public KeyboardMachine::Machine,
|
||||
public Configurable::Device,
|
||||
public CPU::MOS6502::BusHandler,
|
||||
public Inputs::Keyboard,
|
||||
public AppleII::Machine,
|
||||
@@ -57,19 +47,22 @@ class ConcreteMachine:
|
||||
private:
|
||||
struct VideoBusHandler : public AppleII::Video::BusHandler {
|
||||
public:
|
||||
VideoBusHandler(uint8_t *ram) : ram_(ram) {}
|
||||
VideoBusHandler(uint8_t *ram, uint8_t *aux_ram) : ram_(ram), aux_ram_(aux_ram) {}
|
||||
|
||||
uint8_t perform_read(uint16_t address) {
|
||||
return ram_[address];
|
||||
}
|
||||
uint16_t perform_aux_read(uint16_t address) {
|
||||
return static_cast<uint16_t>(ram_[address] | (aux_ram_[address] << 8));
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t *ram_;
|
||||
uint8_t *ram_, *aux_ram_;
|
||||
};
|
||||
|
||||
CPU::MOS6502::Processor<ConcreteMachine, false> m6502_;
|
||||
VideoBusHandler video_bus_handler_;
|
||||
std::unique_ptr<AppleII::Video::Video<VideoBusHandler>> video_;
|
||||
std::unique_ptr<AppleII::Video::Video<VideoBusHandler, is_iie>> video_;
|
||||
int cycles_into_current_line_ = 0;
|
||||
Cycles cycles_since_video_update_;
|
||||
|
||||
@@ -92,6 +85,7 @@ class ConcreteMachine:
|
||||
std::vector<uint8_t> rom_;
|
||||
std::vector<uint8_t> character_rom_;
|
||||
uint8_t keyboard_input_ = 0x00;
|
||||
bool key_is_down_ = false;
|
||||
|
||||
Concurrency::DeferringAsyncTaskQueue audio_queue_;
|
||||
Audio::Toggle audio_toggle_;
|
||||
@@ -136,11 +130,44 @@ class ConcreteMachine:
|
||||
return dynamic_cast<AppleII::DiskIICard *>(cards_[5].get());
|
||||
}
|
||||
|
||||
// MARK: - Memory Map
|
||||
struct MemoryBlock {
|
||||
uint8_t *read_pointer = nullptr;
|
||||
uint8_t *write_pointer = nullptr;
|
||||
} memory_blocks_[4]; // The IO page isn't included.
|
||||
// MARK: - Memory Map.
|
||||
|
||||
/*
|
||||
The Apple II's paging mechanisms are byzantine to say the least. Painful is
|
||||
another appropriate adjective.
|
||||
|
||||
On a II and II+ there are five distinct zones of memory:
|
||||
|
||||
0000 to c000 : the main block of RAM
|
||||
c000 to d000 : the IO area, including card ROMs
|
||||
d000 to e000 : the low ROM area, which can alternatively contain either one of two 4kb blocks of RAM with a language card
|
||||
e000 onward : the rest of ROM, also potentially replaced with RAM by a language card
|
||||
|
||||
On a IIe with auxiliary memory the following orthogonal changes also need to be factored in:
|
||||
|
||||
0000 to 0200 : can be paged independently of the rest of RAM, other than part of the language card area which pages with it
|
||||
0400 to 0800 : the text screen, can be configured to write to auxiliary RAM
|
||||
2000 to 4000 : the graphics screen, which can be configured to write to auxiliary RAM
|
||||
c100 to d000 : can be used to page an additional 3.75kb of ROM, replacing the IO area
|
||||
c300 to c400 : can contain the same 256-byte segment of the ROM as if the whole IO area were switched, but while leaving cards visible in the rest
|
||||
c800 to d000 : can contain ROM separately from the region below c800
|
||||
|
||||
If dealt with as individual blocks in the inner loop, that would therefore imply mapping
|
||||
an address to one of 13 potential pageable zones. So I've gone reductive and surrendered
|
||||
to paging every 6502 page of memory independently. It makes the paging events more expensive,
|
||||
but hopefully more clear.
|
||||
*/
|
||||
uint8_t *read_pages_[256]; // each is a pointer to the 256-block of memory the CPU should read when accessing that page of memory
|
||||
uint8_t *write_pages_[256]; // as per read_pages_, but this is where the CPU should write. If a pointer is nullptr, don't write.
|
||||
void page(int start, int end, uint8_t *read, uint8_t *write) {
|
||||
for(int position = start; position < end; ++position) {
|
||||
read_pages_[position] = read;
|
||||
if(read) read += 256;
|
||||
|
||||
write_pages_[position] = write;
|
||||
if(write) write += 256;
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The language card.
|
||||
struct {
|
||||
@@ -151,28 +178,70 @@ class ConcreteMachine:
|
||||
} language_card_;
|
||||
bool has_language_card_ = true;
|
||||
void set_language_card_paging() {
|
||||
if(has_language_card_ && !language_card_.write) {
|
||||
memory_blocks_[2].write_pointer = &ram_[48*1024 + (language_card_.bank1 ? 0x1000 : 0x0000)];
|
||||
memory_blocks_[3].write_pointer = &ram_[56*1024];
|
||||
} else {
|
||||
memory_blocks_[2].write_pointer = memory_blocks_[3].write_pointer = nullptr;
|
||||
uint8_t *const ram = alternative_zero_page_ ? aux_ram_ : ram_;
|
||||
uint8_t *const rom = is_iie ? &rom_[3840] : rom_.data();
|
||||
|
||||
page(0xd0, 0xe0,
|
||||
language_card_.read ? &ram[language_card_.bank1 ? 0xd000 : 0xc000] : rom,
|
||||
language_card_.write ? nullptr : &ram[language_card_.bank1 ? 0xd000 : 0xc000]);
|
||||
|
||||
page(0xe0, 0x100,
|
||||
language_card_.read ? &ram[0xe000] : &rom[0x1000],
|
||||
language_card_.write ? nullptr : &ram[0xe000]);
|
||||
}
|
||||
|
||||
// MARK - The IIe's ROM controls.
|
||||
bool internal_CX_rom_ = false;
|
||||
bool slot_C3_rom_ = false;
|
||||
bool internal_c8_rom_ = false;
|
||||
|
||||
void set_card_paging() {
|
||||
page(0xc1, 0xc8, internal_CX_rom_ ? rom_.data() : nullptr, nullptr);
|
||||
|
||||
if(!internal_CX_rom_) {
|
||||
if(!slot_C3_rom_) read_pages_[0xc3] = &rom_[0xc300 - 0xc100];
|
||||
}
|
||||
|
||||
if(has_language_card_ && language_card_.read) {
|
||||
memory_blocks_[2].read_pointer = &ram_[48*1024 + (language_card_.bank1 ? 0x1000 : 0x0000)];
|
||||
memory_blocks_[3].read_pointer = &ram_[56*1024];
|
||||
page(0xc8, 0xd0, (internal_CX_rom_ || internal_c8_rom_) ? &rom_[0xc800 - 0xc100] : nullptr, nullptr);
|
||||
}
|
||||
|
||||
// MARK - The IIe's auxiliary RAM controls.
|
||||
bool alternative_zero_page_ = false;
|
||||
void set_zero_page_paging() {
|
||||
if(alternative_zero_page_) {
|
||||
read_pages_[0] = aux_ram_;
|
||||
} else {
|
||||
memory_blocks_[2].read_pointer = rom_.data();
|
||||
memory_blocks_[3].read_pointer = rom_.data() + 0x1000;
|
||||
read_pages_[0] = ram_;
|
||||
}
|
||||
read_pages_[1] = read_pages_[0] + 256;
|
||||
write_pages_[0] = read_pages_[0];
|
||||
write_pages_[1] = read_pages_[1];
|
||||
}
|
||||
|
||||
bool read_auxiliary_memory_ = false;
|
||||
bool write_auxiliary_memory_ = false;
|
||||
void set_main_paging() {
|
||||
page(0x02, 0xc0,
|
||||
read_auxiliary_memory_ ? &aux_ram_[0x0200] : &ram_[0x0200],
|
||||
write_auxiliary_memory_ ? &aux_ram_[0x0200] : &ram_[0x0200]);
|
||||
|
||||
if(video_ && video_->get_80_store()) {
|
||||
bool use_aux_ram = video_->get_page2();
|
||||
page(0x04, 0x08,
|
||||
use_aux_ram ? &aux_ram_[0x0400] : &ram_[0x0400],
|
||||
use_aux_ram ? &aux_ram_[0x0400] : &ram_[0x0400]);
|
||||
|
||||
if(video_->get_high_resolution()) {
|
||||
page(0x20, 0x40,
|
||||
use_aux_ram ? &aux_ram_[0x2000] : &ram_[0x2000],
|
||||
use_aux_ram ? &aux_ram_[0x2000] : &ram_[0x2000]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK - typing
|
||||
std::unique_ptr<Utility::StringSerialiser> string_serialiser_;
|
||||
|
||||
// MARK - quick loading
|
||||
bool should_load_quickly_ = false;
|
||||
|
||||
// MARK - joysticks
|
||||
class Joystick: public Inputs::ConcreteJoystick {
|
||||
public:
|
||||
@@ -220,13 +289,17 @@ class ConcreteMachine:
|
||||
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
||||
bool analogue_channel_is_discharged(size_t channel) {
|
||||
return static_cast<Joystick *>(joysticks_[channel >> 1].get())->axes[channel & 1] < analogue_charge_ + analogue_biases_[channel];
|
||||
return (1.0f - static_cast<Joystick *>(joysticks_[channel >> 1].get())->axes[channel & 1]) < analogue_charge_ + analogue_biases_[channel];
|
||||
}
|
||||
|
||||
// The IIe has three keys that are wired directly to the same input as the joystick buttons.
|
||||
bool open_apple_is_pressed_ = false;
|
||||
bool closed_apple_is_pressed_ = false;
|
||||
|
||||
public:
|
||||
ConcreteMachine(const Analyser::Static::AppleII::Target &target, const ROMMachine::ROMFetcher &rom_fetcher):
|
||||
m6502_(*this),
|
||||
video_bus_handler_(ram_),
|
||||
video_bus_handler_(ram_, aux_ram_),
|
||||
audio_toggle_(audio_queue_),
|
||||
speaker_(audio_toggle_) {
|
||||
// The system's master clock rate.
|
||||
@@ -248,6 +321,7 @@ class ConcreteMachine:
|
||||
|
||||
// Also, start with randomised memory contents.
|
||||
Memory::Fuzz(ram_, sizeof(ram_));
|
||||
Memory::Fuzz(aux_ram_, sizeof(aux_ram_));
|
||||
|
||||
// Add a couple of joysticks.
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
@@ -255,14 +329,22 @@ class ConcreteMachine:
|
||||
|
||||
// Pick the required ROMs.
|
||||
using Target = Analyser::Static::AppleII::Target;
|
||||
std::vector<std::string> rom_names = {"apple2-character.rom"};
|
||||
std::vector<std::string> rom_names;
|
||||
size_t rom_size = 12*1024;
|
||||
switch(target.model) {
|
||||
default:
|
||||
rom_names.push_back("apple2-character.rom");
|
||||
rom_names.push_back("apple2o.rom");
|
||||
break;
|
||||
case Target::Model::IIplus:
|
||||
rom_names.push_back("apple2-character.rom");
|
||||
rom_names.push_back("apple2.rom");
|
||||
break;
|
||||
case Target::Model::IIe:
|
||||
rom_size += 3840;
|
||||
rom_names.push_back("apple2eu-character.rom");
|
||||
rom_names.push_back("apple2eu.rom");
|
||||
break;
|
||||
}
|
||||
const auto roms = rom_fetcher("AppleII", rom_names);
|
||||
|
||||
@@ -270,20 +352,27 @@ class ConcreteMachine:
|
||||
throw ROMMachine::Error::MissingROMs;
|
||||
}
|
||||
|
||||
character_rom_ = std::move(*roms[0]);
|
||||
rom_ = std::move(*roms[1]);
|
||||
if(rom_.size() > 12*1024) {
|
||||
rom_.erase(rom_.begin(), rom_.begin() + static_cast<off_t>(rom_.size()) - 12*1024);
|
||||
if(rom_.size() > rom_size) {
|
||||
rom_.erase(rom_.begin(), rom_.end() - static_cast<off_t>(rom_size));
|
||||
}
|
||||
|
||||
character_rom_ = std::move(*roms[0]);
|
||||
|
||||
if(target.disk_controller != Target::DiskController::None) {
|
||||
// Apple recommended slot 6 for the (first) Disk II.
|
||||
install_card(6, new AppleII::DiskIICard(rom_fetcher, target.disk_controller == Target::DiskController::SixteenSector));
|
||||
}
|
||||
|
||||
// Set up the default memory blocks.
|
||||
memory_blocks_[0].read_pointer = memory_blocks_[0].write_pointer = ram_;
|
||||
memory_blocks_[1].read_pointer = memory_blocks_[1].write_pointer = &ram_[0x200];
|
||||
// Set up the default memory blocks. On a II or II+ these values will never change.
|
||||
// On a IIe they'll be affected by selection of auxiliary RAM.
|
||||
set_main_paging();
|
||||
set_zero_page_paging();
|
||||
|
||||
// Set the whole card area to initially backed by nothing.
|
||||
page(0xc0, 0xd0, nullptr, nullptr);
|
||||
|
||||
// Set proper values for the language card/ROM area.
|
||||
set_language_card_paging();
|
||||
|
||||
insert_media(target.media);
|
||||
@@ -294,7 +383,7 @@ class ConcreteMachine:
|
||||
}
|
||||
|
||||
void setup_output(float aspect_ratio) override {
|
||||
video_.reset(new AppleII::Video::Video<VideoBusHandler>(video_bus_handler_));
|
||||
video_.reset(new AppleII::Video::Video<VideoBusHandler, is_iie>(video_bus_handler_));
|
||||
video_->set_character_rom(character_rom_);
|
||||
}
|
||||
|
||||
@@ -328,108 +417,18 @@ class ConcreteMachine:
|
||||
++ stretched_cycles_since_card_update_;
|
||||
}
|
||||
|
||||
/*
|
||||
There are five distinct zones of memory on an Apple II:
|
||||
|
||||
0000 to 0200 : the zero and stack pages, which can be paged independently on a IIe
|
||||
0200 to c000 : the main block of RAM, which can be paged on a IIe
|
||||
c000 to d000 : the IO area, including card ROMs
|
||||
d000 to e000 : the low ROM area, which can contain indepdently-paged RAM with a language card
|
||||
e000 onward : the rest of ROM, also potentially replaced with RAM by a language card
|
||||
*/
|
||||
uint16_t accessed_address = address;
|
||||
MemoryBlock *block = nullptr;
|
||||
if(address < 0x200) block = &memory_blocks_[0];
|
||||
else if(address < 0xc000) {
|
||||
if(address < 0x6000 && !isReadOperation(operation)) update_video();
|
||||
block = &memory_blocks_[1];
|
||||
accessed_address -= 0x200;
|
||||
}
|
||||
else if(address < 0xd000) block = nullptr;
|
||||
else if(address < 0xe000) {block = &memory_blocks_[2]; accessed_address -= 0xd000; }
|
||||
else { block = &memory_blocks_[3]; accessed_address -= 0xe000; }
|
||||
|
||||
bool has_updated_cards = false;
|
||||
if(block) {
|
||||
if(isReadOperation(operation)) *value = block->read_pointer[accessed_address];
|
||||
else if(block->write_pointer) block->write_pointer[accessed_address] = *value;
|
||||
if(read_pages_[address >> 8]) {
|
||||
if(isReadOperation(operation)) *value = read_pages_[address >> 8][address & 0xff];
|
||||
else if(write_pages_[address >> 8]) write_pages_[address >> 8][address & 0xff] = *value;
|
||||
|
||||
if(should_load_quickly_) {
|
||||
// Check for a prima facie entry into RWTS.
|
||||
if(operation == CPU::MOS6502::BusOperation::ReadOpcode && address == 0xb7b5) {
|
||||
// Grab the IO control block address for inspection.
|
||||
uint16_t io_control_block_address =
|
||||
static_cast<uint16_t>(
|
||||
(m6502_.get_value_of_register(CPU::MOS6502::Register::A) << 8) |
|
||||
m6502_.get_value_of_register(CPU::MOS6502::Register::Y)
|
||||
);
|
||||
|
||||
// Verify that this is table type one, for execution on card six,
|
||||
// against drive 1 or 2, and that the command is either a seek or a sector read.
|
||||
if(
|
||||
ram_[io_control_block_address+0x00] == 0x01 &&
|
||||
ram_[io_control_block_address+0x01] == 0x60 &&
|
||||
ram_[io_control_block_address+0x02] > 0 && ram_[io_control_block_address+0x02] < 3 &&
|
||||
ram_[io_control_block_address+0x0c] < 2
|
||||
) {
|
||||
const uint8_t iob_track = ram_[io_control_block_address+4];
|
||||
const uint8_t iob_sector = ram_[io_control_block_address+5];
|
||||
const uint8_t iob_drive = ram_[io_control_block_address+2] - 1;
|
||||
|
||||
// Get the track identified and store the new head position.
|
||||
auto track = diskii_card()->get_drive(iob_drive).step_to(Storage::Disk::HeadPosition(iob_track));
|
||||
|
||||
// DOS 3.3 keeps the current track (unspecified drive) in 0x478; the current track for drive 1 and drive 2
|
||||
// is also kept in that Disk II card's screen hole.
|
||||
ram_[0x478] = iob_track;
|
||||
if(ram_[io_control_block_address+0x02] == 1) {
|
||||
ram_[0x47e] = iob_track;
|
||||
} else {
|
||||
ram_[0x4fe] = iob_track;
|
||||
}
|
||||
|
||||
// Check whether this is a read, not merely a seek.
|
||||
if(ram_[io_control_block_address+0x0c] == 1) {
|
||||
// Apple the DOS 3.3 formula to map the requested logical sector to a physical sector.
|
||||
const int physical_sector = (iob_sector == 15) ? 15 : ((iob_sector * 13) % 15);
|
||||
|
||||
// Parse the entire track. TODO: cache these.
|
||||
auto sector_map = Storage::Encodings::AppleGCR::sectors_from_segment(
|
||||
Storage::Disk::track_serialisation(*track, Storage::Time(1, 50000)));
|
||||
|
||||
bool found_sector = false;
|
||||
for(const auto &pair: sector_map) {
|
||||
if(pair.second.address.sector == physical_sector) {
|
||||
found_sector = true;
|
||||
|
||||
// Copy the sector contents to their destination.
|
||||
uint16_t target = static_cast<uint16_t>(
|
||||
ram_[io_control_block_address+8] |
|
||||
(ram_[io_control_block_address+9] << 8)
|
||||
);
|
||||
|
||||
for(size_t c = 0; c < 256; ++c) {
|
||||
ram_[target] = pair.second.data[c];
|
||||
++target;
|
||||
}
|
||||
|
||||
// Set no error encountered.
|
||||
ram_[io_control_block_address + 0xd] = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(found_sector) {
|
||||
// Set no error in the flags register too, and RTS.
|
||||
m6502_.set_value_of_register(CPU::MOS6502::Register::Flags, m6502_.get_value_of_register(CPU::MOS6502::Register::Flags) & ~1);
|
||||
*value = 0x60;
|
||||
}
|
||||
} else {
|
||||
// No error encountered; RTS.
|
||||
m6502_.set_value_of_register(CPU::MOS6502::Register::Flags, m6502_.get_value_of_register(CPU::MOS6502::Register::Flags) & ~1);
|
||||
*value = 0x60;
|
||||
}
|
||||
}
|
||||
if(is_iie && address >= 0xc300 && address < 0xd000) {
|
||||
bool internal_c8_rom = internal_c8_rom_;
|
||||
internal_c8_rom |= ((address >> 8) == 0xc3) && !slot_C3_rom_;
|
||||
internal_c8_rom &= (address != 0xcfff);
|
||||
if(internal_c8_rom != internal_c8_rom_) {
|
||||
internal_c8_rom_ = internal_c8_rom;
|
||||
set_card_paging();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -467,12 +466,18 @@ class ConcreteMachine:
|
||||
|
||||
case 0xc061: // Switch input 0.
|
||||
*value &= 0x7f;
|
||||
if(static_cast<Joystick *>(joysticks_[0].get())->buttons[0] || static_cast<Joystick *>(joysticks_[1].get())->buttons[2])
|
||||
if(
|
||||
static_cast<Joystick *>(joysticks_[0].get())->buttons[0] || static_cast<Joystick *>(joysticks_[1].get())->buttons[2] ||
|
||||
(is_iie && open_apple_is_pressed_)
|
||||
)
|
||||
*value |= 0x80;
|
||||
break;
|
||||
case 0xc062: // Switch input 1.
|
||||
*value &= 0x7f;
|
||||
if(static_cast<Joystick *>(joysticks_[0].get())->buttons[1] || static_cast<Joystick *>(joysticks_[1].get())->buttons[1])
|
||||
if(
|
||||
static_cast<Joystick *>(joysticks_[0].get())->buttons[1] || static_cast<Joystick *>(joysticks_[1].get())->buttons[1] ||
|
||||
(is_iie && closed_apple_is_pressed_)
|
||||
)
|
||||
*value |= 0x80;
|
||||
break;
|
||||
case 0xc063: // Switch input 2.
|
||||
@@ -487,13 +492,88 @@ class ConcreteMachine:
|
||||
case 0xc067: { // Analogue input 3.
|
||||
const size_t input = address - 0xc064;
|
||||
*value &= 0x7f;
|
||||
if(analogue_channel_is_discharged(input)) {
|
||||
if(!analogue_channel_is_discharged(input)) {
|
||||
*value |= 0x80;
|
||||
}
|
||||
} break;
|
||||
|
||||
// The IIe-only state reads follow...
|
||||
case 0xc011: if(is_iie) *value = (*value & 0x7f) | (language_card_.bank1 ? 0x80 : 0x00); break;
|
||||
case 0xc012: if(is_iie) *value = (*value & 0x7f) | (language_card_.read ? 0x80 : 0x00); break;
|
||||
case 0xc013: if(is_iie) *value = (*value & 0x7f) | (read_auxiliary_memory_ ? 0x80 : 0x00); break;
|
||||
case 0xc014: if(is_iie) *value = (*value & 0x7f) | (write_auxiliary_memory_ ? 0x80 : 0x00); break;
|
||||
case 0xc015: if(is_iie) *value = (*value & 0x7f) | (internal_CX_rom_ ? 0x80 : 0x00); break;
|
||||
case 0xc016: if(is_iie) *value = (*value & 0x7f) | (alternative_zero_page_ ? 0x80 : 0x00); break;
|
||||
case 0xc017: if(is_iie) *value = (*value & 0x7f) | (slot_C3_rom_ ? 0x80 : 0x00); break;
|
||||
case 0xc018: if(is_iie) *value = (*value & 0x7f) | (video_->get_80_store() ? 0x80 : 0x00); break;
|
||||
case 0xc019: if(is_iie) *value = (*value & 0x7f) | (video_->get_is_vertical_blank(cycles_since_video_update_) ? 0x00 : 0x80); break;
|
||||
case 0xc01a: if(is_iie) *value = (*value & 0x7f) | (video_->get_text() ? 0x80 : 0x00); break;
|
||||
case 0xc01b: if(is_iie) *value = (*value & 0x7f) | (video_->get_mixed() ? 0x80 : 0x00); break;
|
||||
case 0xc01c: if(is_iie) *value = (*value & 0x7f) | (video_->get_page2() ? 0x80 : 0x00); break;
|
||||
case 0xc01d: if(is_iie) *value = (*value & 0x7f) | (video_->get_high_resolution() ? 0x80 : 0x00); break;
|
||||
case 0xc01e: if(is_iie) *value = (*value & 0x7f) | (video_->get_alternative_character_set() ? 0x80 : 0x00); break;
|
||||
case 0xc01f: if(is_iie) *value = (*value & 0x7f) | (video_->get_80_columns() ? 0x80 : 0x00); break;
|
||||
case 0xc07f: if(is_iie) *value = (*value & 0x7f) | (video_->get_double_high_resolution() ? 0x80 : 0x00); break;
|
||||
}
|
||||
} else {
|
||||
// Write-only switches.
|
||||
// Write-only switches. All IIe as currently implemented.
|
||||
if(is_iie) {
|
||||
switch(address) {
|
||||
default: printf("Write %04x?\n", address); break;
|
||||
|
||||
case 0xc000:
|
||||
case 0xc001:
|
||||
update_video();
|
||||
video_->set_80_store(!!(address&1));
|
||||
set_main_paging();
|
||||
break;
|
||||
|
||||
case 0xc002:
|
||||
case 0xc003:
|
||||
read_auxiliary_memory_ = !!(address&1);
|
||||
set_main_paging();
|
||||
break;
|
||||
|
||||
case 0xc004:
|
||||
case 0xc005:
|
||||
write_auxiliary_memory_ = !!(address&1);
|
||||
set_main_paging();
|
||||
break;
|
||||
|
||||
case 0xc006:
|
||||
case 0xc007:
|
||||
internal_CX_rom_ = !!(address&1);
|
||||
set_card_paging();
|
||||
break;
|
||||
|
||||
case 0xc008:
|
||||
case 0xc009:
|
||||
// The alternative zero page setting affects both bank 0 and any RAM
|
||||
// that's paged as though it were on a language card.
|
||||
alternative_zero_page_ = !!(address&1);
|
||||
set_zero_page_paging();
|
||||
set_language_card_paging();
|
||||
break;
|
||||
|
||||
case 0xc00a:
|
||||
case 0xc00b:
|
||||
slot_C3_rom_ = !!(address&1);
|
||||
set_card_paging();
|
||||
break;
|
||||
|
||||
case 0xc00c:
|
||||
case 0xc00d:
|
||||
update_video();
|
||||
video_->set_80_columns(!!(address&1));
|
||||
break;
|
||||
|
||||
case 0xc00e:
|
||||
case 0xc00f:
|
||||
update_video();
|
||||
video_->set_alternative_character_set(!!(address&1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -510,14 +590,33 @@ class ConcreteMachine:
|
||||
} break;
|
||||
|
||||
/* Read-write switches. */
|
||||
case 0xc050: update_video(); video_->set_graphics_mode(); break;
|
||||
case 0xc051: update_video(); video_->set_text_mode(); break;
|
||||
case 0xc052: update_video(); video_->set_mixed_mode(false); break;
|
||||
case 0xc053: update_video(); video_->set_mixed_mode(true); break;
|
||||
case 0xc054: update_video(); video_->set_video_page(0); break;
|
||||
case 0xc055: update_video(); video_->set_video_page(1); break;
|
||||
case 0xc056: update_video(); video_->set_low_resolution(); break;
|
||||
case 0xc057: update_video(); video_->set_high_resolution(); break;
|
||||
case 0xc050:
|
||||
case 0xc051:
|
||||
update_video();
|
||||
video_->set_text(!!(address&1));
|
||||
break;
|
||||
case 0xc052: update_video(); video_->set_mixed(false); break;
|
||||
case 0xc053: update_video(); video_->set_mixed(true); break;
|
||||
case 0xc054:
|
||||
case 0xc055:
|
||||
update_video();
|
||||
video_->set_page2(!!(address&1));
|
||||
set_main_paging();
|
||||
break;
|
||||
case 0xc056:
|
||||
case 0xc057:
|
||||
update_video();
|
||||
video_->set_high_resolution(!!(address&1));
|
||||
set_main_paging();
|
||||
break;
|
||||
|
||||
case 0xc05e:
|
||||
case 0xc05f:
|
||||
if(is_iie) {
|
||||
update_video();
|
||||
video_->set_double_high_resolution(!(address&1));
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xc010:
|
||||
keyboard_input_ &= 0x7f;
|
||||
@@ -525,6 +624,11 @@ class ConcreteMachine:
|
||||
if(!string_serialiser_->advance())
|
||||
string_serialiser_.reset();
|
||||
}
|
||||
|
||||
// On the IIe, reading C010 returns additional key info.
|
||||
if(is_iie && isReadOperation(operation)) {
|
||||
*value = (key_is_down_ ? 0x80 : 0x00) | (keyboard_input_ & 0x7f);
|
||||
}
|
||||
break;
|
||||
|
||||
case 0xc030:
|
||||
@@ -556,6 +660,7 @@ class ConcreteMachine:
|
||||
// "The PRE-WRITE flip-flop is set by an odd read access in the $C08X range. It is reset by an even access or a write access."
|
||||
language_card_.pre_write = isReadOperation(operation) ? (address&1) : false;
|
||||
|
||||
// Apply whatever the net effect of all that is to the memory map.
|
||||
set_language_card_paging();
|
||||
break;
|
||||
}
|
||||
@@ -564,7 +669,7 @@ class ConcreteMachine:
|
||||
Communication with cards follows.
|
||||
*/
|
||||
|
||||
if(address >= 0xc090 && address < 0xc800) {
|
||||
if(!read_pages_[address >> 8] && 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;
|
||||
@@ -589,7 +694,7 @@ class ConcreteMachine:
|
||||
// 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();
|
||||
AppleII::Card *const target = cards_[static_cast<size_t>(card_number)].get();
|
||||
if(target && !is_every_cycle_card(target)) {
|
||||
update_just_in_time_cards();
|
||||
target->perform_bus_operation(select, is_read, address, value);
|
||||
@@ -633,24 +738,46 @@ class ConcreteMachine:
|
||||
m6502_.run_for(cycles);
|
||||
}
|
||||
|
||||
void reset_all_keys() override {
|
||||
open_apple_is_pressed_ = closed_apple_is_pressed_ = key_is_down_ = false;
|
||||
}
|
||||
|
||||
void set_key_pressed(Key key, char value, bool is_pressed) override {
|
||||
if(key == Key::F12) {
|
||||
m6502_.set_reset_line(is_pressed);
|
||||
switch(key) {
|
||||
default: break;
|
||||
case Key::F12:
|
||||
m6502_.set_reset_line(is_pressed);
|
||||
return;
|
||||
case Key::LeftOption:
|
||||
open_apple_is_pressed_ = is_pressed;
|
||||
return;
|
||||
case Key::RightOption:
|
||||
closed_apple_is_pressed_ = is_pressed;
|
||||
return;
|
||||
}
|
||||
|
||||
if(is_pressed) {
|
||||
// If no ASCII value is supplied, look for a few special cases.
|
||||
if(!value) {
|
||||
switch(key) {
|
||||
case Key::Left: value = 8; break;
|
||||
case Key::Right: value = 21; break;
|
||||
case Key::Down: value = 10; break;
|
||||
default: break;
|
||||
}
|
||||
// If no ASCII value is supplied, look for a few special cases.
|
||||
if(!value) {
|
||||
switch(key) {
|
||||
case Key::Left: value = 0x08; break;
|
||||
case Key::Right: value = 0x15; break;
|
||||
case Key::Down: value = 0x0a; break;
|
||||
case Key::Up: value = 0x0b; break;
|
||||
case Key::BackSpace: value = 0x7f; break;
|
||||
default: return;
|
||||
}
|
||||
}
|
||||
|
||||
keyboard_input_ = static_cast<uint8_t>(toupper(value) | 0x80);
|
||||
// Prior to the IIe, the keyboard could produce uppercase only.
|
||||
if(!is_iie) value = static_cast<char>(toupper(value));
|
||||
|
||||
if(is_pressed) {
|
||||
keyboard_input_ = static_cast<uint8_t>(value | 0x80);
|
||||
key_is_down_ = true;
|
||||
} else {
|
||||
if((keyboard_input_ & 0x7f) == value) {
|
||||
key_is_down_ = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,30 +805,6 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Options
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options() override {
|
||||
return AppleII::get_options();
|
||||
}
|
||||
|
||||
void set_selections(const Configurable::SelectionSet &selections_by_option) override {
|
||||
bool quickload;
|
||||
if(Configurable::get_quick_load_tape(selections_by_option, quickload)) {
|
||||
should_load_quickly_ = quickload;
|
||||
}
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_accurate_selections() override {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_quick_load_tape_selection(selection_set, false);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
Configurable::SelectionSet get_user_friendly_selections() override {
|
||||
Configurable::SelectionSet selection_set;
|
||||
Configurable::append_quick_load_tape_selection(selection_set, true);
|
||||
return selection_set;
|
||||
}
|
||||
|
||||
// MARK: JoystickMachine
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
return joysticks_;
|
||||
@@ -715,8 +818,11 @@ using namespace AppleII;
|
||||
Machine *Machine::AppleII(const Analyser::Static::Target *target, const ROMMachine::ROMFetcher &rom_fetcher) {
|
||||
using Target = Analyser::Static::AppleII::Target;
|
||||
const Target *const appleii_target = dynamic_cast<const Target *>(target);
|
||||
return new ConcreteMachine(*appleii_target, rom_fetcher);
|
||||
if(appleii_target->model == Target::Model::IIe) {
|
||||
return new ConcreteMachine<true>(*appleii_target, rom_fetcher);
|
||||
} else {
|
||||
return new ConcreteMachine<false>(*appleii_target, rom_fetcher);
|
||||
}
|
||||
}
|
||||
|
||||
Machine::~Machine() {}
|
||||
|
||||
|
||||
@@ -18,9 +18,6 @@
|
||||
|
||||
namespace AppleII {
|
||||
|
||||
/// @returns The options available for an Apple II.
|
||||
std::vector<std::unique_ptr<Configurable::Option>> get_options();
|
||||
|
||||
class Machine {
|
||||
public:
|
||||
virtual ~Machine();
|
||||
|
||||
@@ -18,7 +18,7 @@ VideoBase::VideoBase() :
|
||||
crt_->set_composite_sampling_function(
|
||||
"float composite_sample(usampler2D sampler, vec2 coordinate, vec2 icoordinate, float phase, float amplitude)"
|
||||
"{"
|
||||
"return texture(sampler, coordinate).r;"
|
||||
"return clamp(texture(sampler, coordinate).r, 0.0, 0.7);"
|
||||
"}");
|
||||
|
||||
// Show only the centre 75% of the TV frame.
|
||||
@@ -31,30 +31,87 @@ Outputs::CRT::CRT *VideoBase::get_crt() {
|
||||
return crt_.get();
|
||||
}
|
||||
|
||||
void VideoBase::set_graphics_mode() {
|
||||
use_graphics_mode_ = true;
|
||||
/*
|
||||
Rote setters and getters.
|
||||
*/
|
||||
void VideoBase::set_alternative_character_set(bool alternative_character_set) {
|
||||
alternative_character_set_ = alternative_character_set;
|
||||
}
|
||||
|
||||
void VideoBase::set_text_mode() {
|
||||
use_graphics_mode_ = false;
|
||||
bool VideoBase::get_alternative_character_set() {
|
||||
return alternative_character_set_;
|
||||
}
|
||||
|
||||
void VideoBase::set_mixed_mode(bool mixed_mode) {
|
||||
mixed_mode_ = mixed_mode;
|
||||
void VideoBase::set_80_columns(bool columns_80) {
|
||||
columns_80_ = columns_80;
|
||||
}
|
||||
|
||||
void VideoBase::set_video_page(int page) {
|
||||
video_page_ = page;
|
||||
bool VideoBase::get_80_columns() {
|
||||
return columns_80_;
|
||||
}
|
||||
|
||||
void VideoBase::set_low_resolution() {
|
||||
graphics_mode_ = GraphicsMode::LowRes;
|
||||
void VideoBase::set_80_store(bool store_80) {
|
||||
store_80_ = store_80;
|
||||
}
|
||||
|
||||
void VideoBase::set_high_resolution() {
|
||||
graphics_mode_ = GraphicsMode::HighRes;
|
||||
bool VideoBase::get_80_store() {
|
||||
return store_80_;
|
||||
}
|
||||
|
||||
void VideoBase::set_page2(bool page2) {
|
||||
page2_ = page2;
|
||||
}
|
||||
|
||||
bool VideoBase::get_page2() {
|
||||
return page2_;
|
||||
}
|
||||
|
||||
void VideoBase::set_text(bool text) {
|
||||
text_ = text;
|
||||
}
|
||||
|
||||
bool VideoBase::get_text() {
|
||||
return text_;
|
||||
}
|
||||
|
||||
void VideoBase::set_mixed(bool mixed) {
|
||||
mixed_ = mixed;
|
||||
}
|
||||
|
||||
bool VideoBase::get_mixed() {
|
||||
return mixed_;
|
||||
}
|
||||
|
||||
void VideoBase::set_high_resolution(bool high_resolution) {
|
||||
high_resolution_ = high_resolution;
|
||||
}
|
||||
|
||||
bool VideoBase::get_high_resolution() {
|
||||
return high_resolution_;
|
||||
}
|
||||
|
||||
void VideoBase::set_double_high_resolution(bool double_high_resolution) {
|
||||
double_high_resolution_ = double_high_resolution;
|
||||
}
|
||||
|
||||
bool VideoBase::get_double_high_resolution() {
|
||||
return double_high_resolution_;
|
||||
}
|
||||
|
||||
void VideoBase::set_character_rom(const std::vector<uint8_t> &character_rom) {
|
||||
character_rom_ = character_rom;
|
||||
|
||||
// Flip all character contents based on the second line of the $ graphic.
|
||||
if(character_rom_[0x121] == 0x3c || character_rom_[0x122] == 0x3c) {
|
||||
for(auto &graphic : character_rom_) {
|
||||
graphic =
|
||||
((graphic & 0x01) ? 0x40 : 0x00) |
|
||||
((graphic & 0x02) ? 0x20 : 0x00) |
|
||||
((graphic & 0x04) ? 0x10 : 0x00) |
|
||||
((graphic & 0x08) ? 0x08 : 0x00) |
|
||||
((graphic & 0x10) ? 0x04 : 0x00) |
|
||||
((graphic & 0x20) ? 0x02 : 0x00) |
|
||||
((graphic & 0x40) ? 0x01 : 0x00);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,21 @@ namespace Video {
|
||||
|
||||
class BusHandler {
|
||||
public:
|
||||
/*!
|
||||
Reads an 8-bit value from the ordinary II/II+ memory pool.
|
||||
*/
|
||||
uint8_t perform_read(uint16_t address) {
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
/*!
|
||||
Reads two 8-bit values, from the same address — one from
|
||||
main RAM, one from auxiliary. Should return as
|
||||
(main) | (aux << 8).
|
||||
*/
|
||||
uint16_t perform_aux_read(uint16_t address) {
|
||||
return 0xffff;
|
||||
}
|
||||
};
|
||||
|
||||
class VideoBase {
|
||||
@@ -31,13 +43,107 @@ class VideoBase {
|
||||
/// @returns The CRT this video feed is feeding.
|
||||
Outputs::CRT::CRT *get_crt();
|
||||
|
||||
// Inputs for the various soft switches.
|
||||
void set_graphics_mode();
|
||||
void set_text_mode();
|
||||
void set_mixed_mode(bool);
|
||||
void set_video_page(int);
|
||||
void set_low_resolution();
|
||||
void set_high_resolution();
|
||||
/*
|
||||
Descriptions for the setters below are taken verbatim from
|
||||
the Apple IIe Technical Reference. Addresses are the conventional
|
||||
locations within the Apple II memory map. Only those which affect
|
||||
video output are implemented here.
|
||||
|
||||
Those registers which don't exist on a II/II+ are marked.
|
||||
*/
|
||||
|
||||
/*!
|
||||
Setter for ALTCHAR ($C00E/$C00F; triggers on write only):
|
||||
|
||||
* Off: display text using primary character set.
|
||||
* On: display text using alternate character set.
|
||||
|
||||
Doesn't exist on a II/II+.
|
||||
*/
|
||||
void set_alternative_character_set(bool);
|
||||
bool get_alternative_character_set();
|
||||
|
||||
/*!
|
||||
Setter for 80COL ($C00C/$C00D; triggers on write only).
|
||||
|
||||
* Off: display 40 columns.
|
||||
* On: display 80 columns.
|
||||
|
||||
Doesn't exist on a II/II+.
|
||||
*/
|
||||
void set_80_columns(bool);
|
||||
bool get_80_columns();
|
||||
|
||||
/*!
|
||||
Setter for 80STORE ($C000/$C001; triggers on write only).
|
||||
|
||||
* Off: cause PAGE2 to select auxiliary RAM.
|
||||
* On: cause PAGE2 to switch main RAM areas.
|
||||
|
||||
Doesn't exist on a II/II+.
|
||||
*/
|
||||
void set_80_store(bool);
|
||||
bool get_80_store();
|
||||
|
||||
/*!
|
||||
Setter for PAGE2 ($C054/$C055; triggers on read or write).
|
||||
|
||||
* Off: select Page 1.
|
||||
* On: select Page 2 or, if 80STORE on, Page 1 in auxiliary memory.
|
||||
|
||||
80STORE doesn't exist on a II/II+; therefore this always selects
|
||||
either Page 1 or Page 2 on those machines.
|
||||
*/
|
||||
void set_page2(bool);
|
||||
bool get_page2();
|
||||
|
||||
/*!
|
||||
Setter for TEXT ($C050/$C051; triggers on read or write).
|
||||
|
||||
* Off: display graphics or, if MIXED on, mixed.
|
||||
* On: display text.
|
||||
*/
|
||||
void set_text(bool);
|
||||
bool get_text();
|
||||
|
||||
/*!
|
||||
Setter for MIXED ($C052/$C053; triggers on read or write).
|
||||
|
||||
* Off: display only text or only graphics.
|
||||
* On: if TEXT off, display text and graphics.
|
||||
*/
|
||||
void set_mixed(bool);
|
||||
bool get_mixed();
|
||||
|
||||
/*!
|
||||
Setter for HIRES ($C056/$C057; triggers on read or write).
|
||||
|
||||
* Off: if TEXT off, display low-resolution graphics.
|
||||
* On: if TEXT off, display high-resolution or, if DHIRES on, double high-resolution graphics.
|
||||
|
||||
DHIRES doesn't exist on a II/II+; therefore this always selects
|
||||
either high- or low-resolution graphics on those machines.
|
||||
|
||||
Despite Apple's documentation, the IIe also supports double low-resolution
|
||||
graphics, which are the 80-column analogue to ordinary low-resolution 40-column
|
||||
low-resolution graphics.
|
||||
*/
|
||||
void set_high_resolution(bool);
|
||||
bool get_high_resolution();
|
||||
|
||||
/*!
|
||||
Setter for DHIRES ($C05E/$C05F; triggers on write only).
|
||||
|
||||
* On: turn on double-high resolution.
|
||||
* Off: turn off double-high resolution.
|
||||
|
||||
DHIRES doesn't exist on a II/II+. On the IIe there is another
|
||||
register usually grouped with the graphics setters called IOUDIS
|
||||
that affects visibility of this switch. But it has no effect on
|
||||
video, so it's not modelled by this class.
|
||||
*/
|
||||
void set_double_high_resolution(bool);
|
||||
bool get_double_high_resolution();
|
||||
|
||||
// Setup for text mode.
|
||||
void set_character_rom(const std::vector<uint8_t> &);
|
||||
@@ -45,25 +151,47 @@ class VideoBase {
|
||||
protected:
|
||||
std::unique_ptr<Outputs::CRT::CRT> crt_;
|
||||
|
||||
// State affecting output video stream generation.
|
||||
uint8_t *pixel_pointer_ = nullptr;
|
||||
int pixel_pointer_column_ = 0;
|
||||
bool pixels_are_high_density_ = false;
|
||||
|
||||
int video_page_ = 0;
|
||||
// State affecting logical state.
|
||||
int row_ = 0, column_ = 0, flash_ = 0;
|
||||
std::vector<uint8_t> character_rom_;
|
||||
|
||||
// Enumerates all Apple II and IIe display modes.
|
||||
enum class GraphicsMode {
|
||||
LowRes,
|
||||
DoubleLowRes,
|
||||
HighRes,
|
||||
Text
|
||||
} graphics_mode_ = GraphicsMode::LowRes;
|
||||
bool use_graphics_mode_ = false;
|
||||
bool mixed_mode_ = false;
|
||||
DoubleHighRes,
|
||||
Text,
|
||||
DoubleText
|
||||
};
|
||||
bool is_text_mode(GraphicsMode m) { return m >= GraphicsMode::Text; }
|
||||
|
||||
// Various soft-switch values.
|
||||
bool alternative_character_set_ = false;
|
||||
bool columns_80_ = false;
|
||||
bool store_80_ = false;
|
||||
bool page2_ = false;
|
||||
bool text_ = true;
|
||||
bool mixed_ = false;
|
||||
bool high_resolution_ = false;
|
||||
bool double_high_resolution_ = false;
|
||||
|
||||
// Graphics carry is the final level output in a fetch window;
|
||||
// it carries on into the next if it's high resolution with
|
||||
// the delay bit set.
|
||||
uint8_t graphics_carry_ = 0;
|
||||
|
||||
// This holds a copy of the character ROM. The regular character
|
||||
// set is assumed to be in the first 64*8 bytes; the alternative
|
||||
// is in the 128*8 bytes after that.
|
||||
std::vector<uint8_t> character_rom_;
|
||||
};
|
||||
|
||||
template <class BusHandler> class Video: public VideoBase {
|
||||
template <class BusHandler, bool is_iie> class Video: public VideoBase {
|
||||
public:
|
||||
/// Constructs an instance of the video feed; a CRT is also created.
|
||||
Video(BusHandler &bus_handler) :
|
||||
@@ -108,15 +236,14 @@ template <class BusHandler> class Video: public VideoBase {
|
||||
crt_->output_sync(static_cast<unsigned int>(cycles_this_line) * 14);
|
||||
}
|
||||
} else {
|
||||
const GraphicsMode line_mode = use_graphics_mode_ ? graphics_mode_ : GraphicsMode::Text;
|
||||
const GraphicsMode line_mode = graphics_mode(row_);
|
||||
|
||||
// The first 40 columns are submitted to the CRT only upon completion;
|
||||
// they'll be either graphics or blank, depending on which side we are
|
||||
// of line 192.
|
||||
if(column_ < 40) {
|
||||
if(row_ < 192) {
|
||||
GraphicsMode pixel_mode = (!mixed_mode_ || row_ < 160) ? line_mode : GraphicsMode::Text;
|
||||
bool requires_high_density = pixel_mode != GraphicsMode::Text;
|
||||
const bool requires_high_density = line_mode != GraphicsMode::Text;
|
||||
if(!column_ || requires_high_density != pixels_are_high_density_) {
|
||||
if(column_) output_data_to_column(column_);
|
||||
pixel_pointer_ = crt_->allocate_write_area(561);
|
||||
@@ -129,21 +256,25 @@ template <class BusHandler> class Video: public VideoBase {
|
||||
const int character_row = row_ >> 3;
|
||||
const int pixel_row = row_ & 7;
|
||||
const uint16_t row_address = static_cast<uint16_t>((character_row >> 3) * 40 + ((character_row&7) << 7));
|
||||
const uint16_t text_address = static_cast<uint16_t>(((video_page_+1) * 0x400) + row_address);
|
||||
const uint16_t text_address = static_cast<uint16_t>(((video_page()+1) * 0x400) + row_address);
|
||||
|
||||
switch(pixel_mode) {
|
||||
switch(line_mode) {
|
||||
case GraphicsMode::Text: {
|
||||
const uint8_t inverses[] = {
|
||||
0xff,
|
||||
static_cast<uint8_t>((flash_ / flash_length) * 0xff),
|
||||
alternative_character_set_ ? static_cast<uint8_t>(0xff) : static_cast<uint8_t>((flash_ / flash_length) * 0xff),
|
||||
0x00,
|
||||
0x00
|
||||
};
|
||||
const uint8_t masks[] = {
|
||||
alternative_character_set_ ? static_cast<uint8_t>(0x7f) : static_cast<uint8_t>(0x3f),
|
||||
is_iie ? 0x7f : 0x3f,
|
||||
};
|
||||
for(int c = column_; c < pixel_end; ++c) {
|
||||
const uint8_t character = bus_handler_.perform_read(static_cast<uint16_t>(text_address + c));
|
||||
const std::size_t character_address = static_cast<std::size_t>(((character & 0x3f) << 3) + pixel_row);
|
||||
|
||||
const uint8_t character_pattern = character_rom_[character_address] ^ inverses[character >> 6];
|
||||
const uint8_t xor_mask = inverses[character >> 6];
|
||||
const std::size_t character_address = static_cast<std::size_t>(((character & masks[character >> 7]) << 3) + pixel_row);
|
||||
const uint8_t character_pattern = character_rom_[character_address] ^ xor_mask;
|
||||
|
||||
// The character ROM is output MSB to LSB rather than LSB to MSB.
|
||||
pixel_pointer_[0] = character_pattern & 0x40;
|
||||
@@ -153,11 +284,86 @@ template <class BusHandler> class Video: public VideoBase {
|
||||
pixel_pointer_[4] = character_pattern & 0x04;
|
||||
pixel_pointer_[5] = character_pattern & 0x02;
|
||||
pixel_pointer_[6] = character_pattern & 0x01;
|
||||
graphics_carry_ = character_pattern & 0x40;
|
||||
graphics_carry_ = character_pattern & 0x01;
|
||||
pixel_pointer_ += 7;
|
||||
}
|
||||
} break;
|
||||
|
||||
case GraphicsMode::DoubleText: {
|
||||
const uint8_t inverses[] = {
|
||||
0xff,
|
||||
alternative_character_set_ ? static_cast<uint8_t>(0xff) : static_cast<uint8_t>((flash_ / flash_length) * 0xff),
|
||||
0x00,
|
||||
0x00
|
||||
};
|
||||
const uint8_t masks[] = {
|
||||
alternative_character_set_ ? static_cast<uint8_t>(0x7f) : static_cast<uint8_t>(0x3f),
|
||||
is_iie ? 0x7f : 0x3f,
|
||||
};
|
||||
for(int c = column_; c < pixel_end; ++c) {
|
||||
const uint16_t characters = bus_handler_.perform_aux_read(static_cast<uint16_t>(text_address + c));
|
||||
const std::size_t character_addresses[2] = {
|
||||
static_cast<std::size_t>((((characters >> 8) & masks[characters >> 15]) << 3) + pixel_row),
|
||||
static_cast<std::size_t>(((characters & masks[(characters >> 7)&1]) << 3) + pixel_row),
|
||||
};
|
||||
|
||||
const uint8_t character_patterns[2] = {
|
||||
static_cast<uint8_t>(character_rom_[character_addresses[0]] ^ inverses[(characters >> 14) & 3]),
|
||||
static_cast<uint8_t>(character_rom_[character_addresses[1]] ^ inverses[(characters >> 6) & 3]),
|
||||
};
|
||||
|
||||
// The character ROM is output MSB to LSB rather than LSB to MSB.
|
||||
pixel_pointer_[0] = character_patterns[0] & 0x40;
|
||||
pixel_pointer_[1] = character_patterns[0] & 0x20;
|
||||
pixel_pointer_[2] = character_patterns[0] & 0x10;
|
||||
pixel_pointer_[3] = character_patterns[0] & 0x08;
|
||||
pixel_pointer_[4] = character_patterns[0] & 0x04;
|
||||
pixel_pointer_[5] = character_patterns[0] & 0x02;
|
||||
pixel_pointer_[6] = character_patterns[0] & 0x01;
|
||||
pixel_pointer_[7] = character_patterns[1] & 0x40;
|
||||
pixel_pointer_[8] = character_patterns[1] & 0x20;
|
||||
pixel_pointer_[9] = character_patterns[1] & 0x10;
|
||||
pixel_pointer_[10] = character_patterns[1] & 0x08;
|
||||
pixel_pointer_[11] = character_patterns[1] & 0x04;
|
||||
pixel_pointer_[12] = character_patterns[1] & 0x02;
|
||||
pixel_pointer_[13] = character_patterns[1] & 0x01;
|
||||
graphics_carry_ = character_patterns[1] & 0x01;
|
||||
pixel_pointer_ += 14;
|
||||
}
|
||||
} break;
|
||||
|
||||
case GraphicsMode::DoubleLowRes: {
|
||||
const int row_shift = (row_&4);
|
||||
for(int c = column_; c < pixel_end; ++c) {
|
||||
const uint16_t nibble = (bus_handler_.perform_aux_read(static_cast<uint16_t>(text_address + c)) >> row_shift) & 0xf0f;
|
||||
|
||||
if(c&1) {
|
||||
pixel_pointer_[0] = pixel_pointer_[4] = (nibble >> 8) & 4;
|
||||
pixel_pointer_[1] = pixel_pointer_[5] = (nibble >> 8) & 8;
|
||||
pixel_pointer_[2] = pixel_pointer_[6] = (nibble >> 8) & 1;
|
||||
pixel_pointer_[3] = (nibble >> 8) & 2;
|
||||
|
||||
pixel_pointer_[8] = pixel_pointer_[12] = nibble & 4;
|
||||
pixel_pointer_[9] = pixel_pointer_[13] = nibble & 8;
|
||||
pixel_pointer_[10] = nibble & 1;
|
||||
pixel_pointer_[7] = pixel_pointer_[11] = nibble & 2;
|
||||
graphics_carry_ = nibble & 8;
|
||||
} else {
|
||||
pixel_pointer_[0] = pixel_pointer_[4] = (nibble >> 8) & 1;
|
||||
pixel_pointer_[1] = pixel_pointer_[5] = (nibble >> 8) & 2;
|
||||
pixel_pointer_[2] = pixel_pointer_[6] = (nibble >> 8) & 4;
|
||||
pixel_pointer_[3] = (nibble >> 8) & 8;
|
||||
|
||||
pixel_pointer_[8] = pixel_pointer_[12] = nibble & 1;
|
||||
pixel_pointer_[9] = pixel_pointer_[13] = nibble & 2;
|
||||
pixel_pointer_[10] = nibble & 4;
|
||||
pixel_pointer_[7] = pixel_pointer_[11] = nibble & 8;
|
||||
graphics_carry_ = nibble & 2;
|
||||
}
|
||||
pixel_pointer_ += 14;
|
||||
}
|
||||
} break;
|
||||
|
||||
case GraphicsMode::LowRes: {
|
||||
const int row_shift = (row_&4);
|
||||
// TODO: decompose into two loops, possibly.
|
||||
@@ -184,7 +390,7 @@ template <class BusHandler> class Video: public VideoBase {
|
||||
} break;
|
||||
|
||||
case GraphicsMode::HighRes: {
|
||||
const uint16_t graphics_address = static_cast<uint16_t>(((video_page_+1) * 0x2000) + row_address + ((pixel_row&7) << 10));
|
||||
const uint16_t graphics_address = static_cast<uint16_t>(((video_page()+1) * 0x2000) + row_address + ((pixel_row&7) << 10));
|
||||
for(int c = column_; c < pixel_end; ++c) {
|
||||
const uint8_t graphic = bus_handler_.perform_read(static_cast<uint16_t>(graphics_address + c));
|
||||
|
||||
@@ -212,6 +418,30 @@ template <class BusHandler> class Video: public VideoBase {
|
||||
pixel_pointer_ += 14;
|
||||
}
|
||||
} break;
|
||||
|
||||
case GraphicsMode::DoubleHighRes: {
|
||||
const uint16_t graphics_address = static_cast<uint16_t>(((video_page()+1) * 0x2000) + row_address + ((pixel_row&7) << 10));
|
||||
for(int c = column_; c < pixel_end; ++c) {
|
||||
const uint16_t graphic = bus_handler_.perform_aux_read(static_cast<uint16_t>(graphics_address + c));
|
||||
|
||||
pixel_pointer_[0] = graphics_carry_;
|
||||
pixel_pointer_[1] = (graphic >> 8) & 0x01;
|
||||
pixel_pointer_[2] = (graphic >> 8) & 0x02;
|
||||
pixel_pointer_[3] = (graphic >> 8) & 0x04;
|
||||
pixel_pointer_[4] = (graphic >> 8) & 0x08;
|
||||
pixel_pointer_[5] = (graphic >> 8) & 0x10;
|
||||
pixel_pointer_[6] = (graphic >> 8) & 0x20;
|
||||
pixel_pointer_[7] = (graphic >> 8) & 0x40;
|
||||
pixel_pointer_[8] = graphic & 0x01;
|
||||
pixel_pointer_[9] = graphic & 0x02;
|
||||
pixel_pointer_[10] = graphic & 0x04;
|
||||
pixel_pointer_[11] = graphic & 0x08;
|
||||
pixel_pointer_[12] = graphic & 0x10;
|
||||
pixel_pointer_[13] = graphic & 0x20;
|
||||
graphics_carry_ = graphic & 0x40;
|
||||
pixel_pointer_ += 14;
|
||||
}
|
||||
} break;
|
||||
}
|
||||
|
||||
if(ending_column >= 40) {
|
||||
@@ -242,11 +472,11 @@ template <class BusHandler> class Video: public VideoBase {
|
||||
}
|
||||
|
||||
int second_blank_start;
|
||||
if(line_mode != GraphicsMode::Text && (!mixed_mode_ || row_ < 159 || row_ >= 192)) {
|
||||
if(!is_text_mode(graphics_mode(row_+1))) {
|
||||
const int colour_burst_start = std::max(first_sync_column + sync_length + 1, column_);
|
||||
const int colour_burst_end = std::min(first_sync_column + sync_length + 4, ending_column);
|
||||
if(colour_burst_end > colour_burst_start) {
|
||||
crt_->output_default_colour_burst(static_cast<unsigned int>(colour_burst_end - colour_burst_start) * 14);
|
||||
crt_->output_colour_burst(static_cast<unsigned int>(colour_burst_end - colour_burst_start) * 14, 128);
|
||||
}
|
||||
|
||||
second_blank_start = std::max(first_sync_column + 7, column_);
|
||||
@@ -310,16 +540,49 @@ template <class BusHandler> class Video: public VideoBase {
|
||||
return bus_handler_.perform_read(read_address);
|
||||
}
|
||||
|
||||
/*!
|
||||
@returns @c true if the display will be within vertical blank at now + @c offset; @c false otherwise.
|
||||
*/
|
||||
bool get_is_vertical_blank(Cycles offset) {
|
||||
// Map that backwards from the internal pixels-at-start generation to pixels-at-end
|
||||
// (so what was column 0 is now column 25).
|
||||
int mapped_column = column_ + offset.as_int();
|
||||
|
||||
// Map that backwards from the internal pixels-at-start generation to pixels-at-end
|
||||
// (so what was column 0 is now column 25).
|
||||
mapped_column += 25;
|
||||
|
||||
// Apply carry into the row counter and test it for location.
|
||||
int mapped_row = row_ + (mapped_column / 65);
|
||||
return (mapped_row % 262) >= 192;
|
||||
}
|
||||
|
||||
private:
|
||||
GraphicsMode graphics_mode(int row) {
|
||||
if(text_) return columns_80_ ? GraphicsMode::DoubleText : GraphicsMode::Text;
|
||||
if(mixed_ && row >= 160 && row < 192) {
|
||||
return (columns_80_ || double_high_resolution_) ? GraphicsMode::DoubleText : GraphicsMode::Text;
|
||||
}
|
||||
if(high_resolution_) {
|
||||
return double_high_resolution_ ? GraphicsMode::DoubleHighRes : GraphicsMode::HighRes;
|
||||
} else {
|
||||
return double_high_resolution_ ? GraphicsMode::DoubleLowRes : GraphicsMode::LowRes;
|
||||
}
|
||||
}
|
||||
|
||||
int video_page() {
|
||||
return (store_80_ || !page2_) ? 0 : 1;
|
||||
}
|
||||
|
||||
uint16_t get_row_address(int row) {
|
||||
const int character_row = row >> 3;
|
||||
const int pixel_row = row & 7;
|
||||
const uint16_t row_address = static_cast<uint16_t>((character_row >> 3) * 40 + ((character_row&7) << 7));
|
||||
|
||||
GraphicsMode pixel_mode = ((!mixed_mode_ || row < 160) && use_graphics_mode_) ? graphics_mode_ : GraphicsMode::Text;
|
||||
return (pixel_mode == GraphicsMode::HighRes) ?
|
||||
static_cast<uint16_t>(((video_page_+1) * 0x2000) + row_address + ((pixel_row&7) << 10)) :
|
||||
static_cast<uint16_t>(((video_page_+1) * 0x400) + row_address);
|
||||
const GraphicsMode pixel_mode = graphics_mode(row);
|
||||
return ((pixel_mode == GraphicsMode::HighRes) || (pixel_mode == GraphicsMode::DoubleHighRes)) ?
|
||||
static_cast<uint16_t>(((video_page()+1) * 0x2000) + row_address + ((pixel_row&7) << 10)) :
|
||||
static_cast<uint16_t>(((video_page()+1) * 0x400) + row_address);
|
||||
}
|
||||
|
||||
static const int flash_length = 8406;
|
||||
|
||||
@@ -404,7 +404,7 @@ unsigned int VideoOutput::get_cycles_until_next_ram_availability(int from_time)
|
||||
if(current_line >= output_position_line) {
|
||||
// Get the number of lines since then if still in the same frame.
|
||||
int lines_since_output_position = current_line - output_position_line;
|
||||
|
||||
|
||||
// Therefore get the character row at the proposed time, modulo 10.
|
||||
implied_row = (current_character_row_ + lines_since_output_position) % 10;
|
||||
} else {
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
|
||||
#include "../../Activity/Source.hpp"
|
||||
#include "../CRTMachine.hpp"
|
||||
#include "../JoystickMachine.hpp"
|
||||
#include "../MediaTarget.hpp"
|
||||
#include "../KeyboardMachine.hpp"
|
||||
|
||||
@@ -54,13 +55,19 @@ std::vector<std::unique_ptr<Configurable::Option>> get_options() {
|
||||
|
||||
class AYPortHandler: public GI::AY38910::PortHandler {
|
||||
public:
|
||||
AYPortHandler(Storage::Tape::BinaryTapePlayer &tape_player) : tape_player_(tape_player) {}
|
||||
AYPortHandler(Storage::Tape::BinaryTapePlayer &tape_player) : tape_player_(tape_player) {
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
joysticks_.emplace_back(new Joystick);
|
||||
}
|
||||
|
||||
void set_port_output(bool port_b, uint8_t value) {
|
||||
if(port_b) {
|
||||
// Bits 0-3: touchpad handshaking (?)
|
||||
// Bit 4-5: monostable timer pulses
|
||||
|
||||
// Bit 6: joystick select
|
||||
selected_joystick_ = (value >> 6) & 1;
|
||||
|
||||
// Bit 7: code LED, if any
|
||||
}
|
||||
}
|
||||
@@ -69,15 +76,60 @@ class AYPortHandler: public GI::AY38910::PortHandler {
|
||||
if(!port_b) {
|
||||
// Bits 0-5: Joystick (up, down, left, right, A, B)
|
||||
// Bit 6: keyboard switch (not universal)
|
||||
|
||||
// Bit 7: tape input
|
||||
return 0x7f | (tape_player_.get_input() ? 0x00 : 0x80);
|
||||
return
|
||||
(static_cast<Joystick *>(joysticks_[selected_joystick_].get())->get_state() & 0x3f) |
|
||||
0x40 |
|
||||
(tape_player_.get_input() ? 0x00 : 0x80);
|
||||
}
|
||||
return 0xff;
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() {
|
||||
return joysticks_;
|
||||
}
|
||||
|
||||
private:
|
||||
Storage::Tape::BinaryTapePlayer &tape_player_;
|
||||
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> joysticks_;
|
||||
size_t selected_joystick_ = 0;
|
||||
class Joystick: public Inputs::ConcreteJoystick {
|
||||
public:
|
||||
Joystick() :
|
||||
ConcreteJoystick({
|
||||
Input(Input::Up),
|
||||
Input(Input::Down),
|
||||
Input(Input::Left),
|
||||
Input(Input::Right),
|
||||
Input(Input::Fire, 0),
|
||||
Input(Input::Fire, 1),
|
||||
}) {}
|
||||
|
||||
void did_set_input(const Input &input, bool is_active) override {
|
||||
uint8_t mask = 0;
|
||||
switch(input.type) {
|
||||
default: return;
|
||||
case Input::Up: mask = 0x01; break;
|
||||
case Input::Down: mask = 0x02; break;
|
||||
case Input::Left: mask = 0x04; break;
|
||||
case Input::Right: mask = 0x08; break;
|
||||
case Input::Fire:
|
||||
if(input.info.control.index >= 2) return;
|
||||
mask = input.info.control.index ? 0x20 : 0x10;
|
||||
break;
|
||||
}
|
||||
|
||||
if(is_active) state_ &= ~mask; else state_ |= mask;
|
||||
}
|
||||
|
||||
uint8_t get_state() {
|
||||
return state_;
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t state_ = 0xff;
|
||||
};
|
||||
};
|
||||
|
||||
class ConcreteMachine:
|
||||
@@ -87,6 +139,7 @@ class ConcreteMachine:
|
||||
public MediaTarget::Machine,
|
||||
public KeyboardMachine::Machine,
|
||||
public Configurable::Device,
|
||||
public JoystickMachine::Machine,
|
||||
public MemoryMap,
|
||||
public ClockingHint::Observer,
|
||||
public Activity::Source {
|
||||
@@ -561,6 +614,11 @@ class ConcreteMachine:
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Joysticks
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
|
||||
return ay_port_handler_.get_joysticks();
|
||||
}
|
||||
|
||||
private:
|
||||
DiskROM *get_disk_rom() {
|
||||
return dynamic_cast<DiskROM *>(memory_slots_[2].handler.get());
|
||||
|
||||
@@ -130,7 +130,6 @@ std::map<std::string, std::vector<std::unique_ptr<Configurable::Option>>> Machin
|
||||
std::map<std::string, std::vector<std::unique_ptr<Configurable::Option>>> options;
|
||||
|
||||
options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::AmstradCPC), AmstradCPC::get_options()));
|
||||
options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::AppleII), AppleII::get_options()));
|
||||
options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::Electron), Electron::get_options()));
|
||||
options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::MSX), MSX::get_options()));
|
||||
options.emplace(std::make_pair(LongNameForTargetMachine(Analyser::Machine::Oric), Oric::get_options()));
|
||||
|
||||
@@ -605,6 +605,7 @@
|
||||
4BBF99151C8FBA6F0075DAFB /* CRTOpenGL.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF990A1C8FBA6F0075DAFB /* CRTOpenGL.cpp */; };
|
||||
4BBF99181C8FBA6F0075DAFB /* TextureTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF99121C8FBA6F0075DAFB /* TextureTarget.cpp */; };
|
||||
4BBFBB6C1EE8401E00C01E7A /* ZX8081.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBFBB6A1EE8401E00C01E7A /* ZX8081.cpp */; };
|
||||
4BBFE83D21015D9C00BF1C40 /* CSJoystickManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 4BBFE83C21015D9C00BF1C40 /* CSJoystickManager.m */; };
|
||||
4BBFFEE61F7B27F1005F3FEB /* TrackSerialiser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBFFEE51F7B27F1005F3FEB /* TrackSerialiser.cpp */; };
|
||||
4BC39568208EE6CF0044766B /* DiskIICard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC39566208EE6CF0044766B /* DiskIICard.cpp */; };
|
||||
4BC39569208EE6CF0044766B /* DiskIICard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC39566208EE6CF0044766B /* DiskIICard.cpp */; };
|
||||
@@ -614,6 +615,8 @@
|
||||
4BC751B21D157E61006C31D9 /* 6522Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC751B11D157E61006C31D9 /* 6522Tests.swift */; };
|
||||
4BC76E691C98E31700E6EF73 /* FIRFilter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC76E671C98E31700E6EF73 /* FIRFilter.cpp */; };
|
||||
4BC76E6B1C98F43700E6EF73 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BC76E6A1C98F43700E6EF73 /* Accelerate.framework */; };
|
||||
4BC891AD20F6EAB300EDE5B3 /* Rectangle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC891AB20F6EAB300EDE5B3 /* Rectangle.cpp */; };
|
||||
4BC891AE20F6EAB300EDE5B3 /* Rectangle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC891AB20F6EAB300EDE5B3 /* Rectangle.cpp */; };
|
||||
4BC91B831D1F160E00884B76 /* CommodoreTAP.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC91B811D1F160E00884B76 /* CommodoreTAP.cpp */; };
|
||||
4BC9DF451D044FCA00F44158 /* ROMImages in Resources */ = {isa = PBXBuildFile; fileRef = 4BC9DF441D044FCA00F44158 /* ROMImages */; };
|
||||
4BC9DF4F1D04691600F44158 /* 6560.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC9DF4D1D04691600F44158 /* 6560.cpp */; };
|
||||
@@ -1343,6 +1346,8 @@
|
||||
4BBF99191C8FC2750075DAFB /* CRTTypes.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = CRTTypes.hpp; sourceTree = "<group>"; };
|
||||
4BBFBB6A1EE8401E00C01E7A /* ZX8081.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = ZX8081.cpp; path = Parsers/ZX8081.cpp; sourceTree = "<group>"; };
|
||||
4BBFBB6B1EE8401E00C01E7A /* ZX8081.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = ZX8081.hpp; path = Parsers/ZX8081.hpp; sourceTree = "<group>"; };
|
||||
4BBFE83C21015D9C00BF1C40 /* CSJoystickManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CSJoystickManager.m; sourceTree = "<group>"; };
|
||||
4BBFE83E21015DAE00BF1C40 /* CSJoystickManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CSJoystickManager.h; sourceTree = "<group>"; };
|
||||
4BBFFEE51F7B27F1005F3FEB /* TrackSerialiser.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = TrackSerialiser.cpp; sourceTree = "<group>"; };
|
||||
4BC39565208EDFCE0044766B /* Card.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Card.hpp; sourceTree = "<group>"; };
|
||||
4BC39566208EE6CF0044766B /* DiskIICard.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = DiskIICard.cpp; sourceTree = "<group>"; };
|
||||
@@ -1356,6 +1361,8 @@
|
||||
4BC76E671C98E31700E6EF73 /* FIRFilter.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = FIRFilter.cpp; sourceTree = "<group>"; };
|
||||
4BC76E681C98E31700E6EF73 /* FIRFilter.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = FIRFilter.hpp; sourceTree = "<group>"; };
|
||||
4BC76E6A1C98F43700E6EF73 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; };
|
||||
4BC891AB20F6EAB300EDE5B3 /* Rectangle.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = Rectangle.cpp; sourceTree = "<group>"; };
|
||||
4BC891AC20F6EAB300EDE5B3 /* Rectangle.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Rectangle.hpp; sourceTree = "<group>"; };
|
||||
4BC91B811D1F160E00884B76 /* CommodoreTAP.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CommodoreTAP.cpp; sourceTree = "<group>"; };
|
||||
4BC91B821D1F160E00884B76 /* CommodoreTAP.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CommodoreTAP.hpp; sourceTree = "<group>"; };
|
||||
4BC9DF441D044FCA00F44158 /* ROMImages */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ROMImages; path = ../../../../ROMImages; sourceTree = "<group>"; };
|
||||
@@ -2756,6 +2763,7 @@
|
||||
4BB73EA01B587A5100552FC2 /* Clock Signal */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4BBFE83B21015D9C00BF1C40 /* Joystick Manager */,
|
||||
4BB73ECF1B587A6700552FC2 /* Clock Signal.entitlements */,
|
||||
4B1414501B58848C00E04248 /* ClockSignal-Bridging-Header.h */,
|
||||
4BB73EAD1B587A5100552FC2 /* Info.plist */,
|
||||
@@ -2896,6 +2904,7 @@
|
||||
children = (
|
||||
4B5073051DDD3B9400C48FBD /* ArrayBuilder.cpp */,
|
||||
4BBF990A1C8FBA6F0075DAFB /* CRTOpenGL.cpp */,
|
||||
4BC891AB20F6EAB300EDE5B3 /* Rectangle.cpp */,
|
||||
4BBF99081C8FBA6F0075DAFB /* TextureBuilder.cpp */,
|
||||
4BBF99121C8FBA6F0075DAFB /* TextureTarget.cpp */,
|
||||
4B5073061DDD3B9400C48FBD /* ArrayBuilder.hpp */,
|
||||
@@ -2903,6 +2912,7 @@
|
||||
4BBF990B1C8FBA6F0075DAFB /* CRTOpenGL.hpp */,
|
||||
4BBF990E1C8FBA6F0075DAFB /* Flywheel.hpp */,
|
||||
4BBF990F1C8FBA6F0075DAFB /* OpenGL.hpp */,
|
||||
4BC891AC20F6EAB300EDE5B3 /* Rectangle.hpp */,
|
||||
4BBF99091C8FBA6F0075DAFB /* TextureBuilder.hpp */,
|
||||
4BBF99131C8FBA6F0075DAFB /* TextureTarget.hpp */,
|
||||
4BC3B74C1CD194CC00F86E85 /* Shaders */,
|
||||
@@ -2910,6 +2920,15 @@
|
||||
path = Internals;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4BBFE83B21015D9C00BF1C40 /* Joystick Manager */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4BBFE83C21015D9C00BF1C40 /* CSJoystickManager.m */,
|
||||
4BBFE83E21015DAE00BF1C40 /* CSJoystickManager.h */,
|
||||
);
|
||||
path = "Joystick Manager";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4BC3B74C1CD194CC00F86E85 /* Shaders */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -3614,6 +3633,7 @@
|
||||
4B055AD81FAE9B180060FFFF /* Video.cpp in Sources */,
|
||||
4B89452F201967B4007DE474 /* StaticAnalyser.cpp in Sources */,
|
||||
4B894531201967B4007DE474 /* StaticAnalyser.cpp in Sources */,
|
||||
4BC891AE20F6EAB300EDE5B3 /* Rectangle.cpp in Sources */,
|
||||
4B894539201967B4007DE474 /* Tape.cpp in Sources */,
|
||||
4B055AE51FAE9B6F0060FFFF /* IntermediateShader.cpp in Sources */,
|
||||
4B15A9FD208249BB005E6C8D /* StaticAnalyser.cpp in Sources */,
|
||||
@@ -3817,6 +3837,7 @@
|
||||
4B4518A21F75FD1C00926311 /* G64.cpp in Sources */,
|
||||
4B89452C201967B4007DE474 /* Tape.cpp in Sources */,
|
||||
4B448E811F1C45A00009ABD6 /* TZX.cpp in Sources */,
|
||||
4BBFE83D21015D9C00BF1C40 /* CSJoystickManager.m in Sources */,
|
||||
4BEBFB512002DB30000708CC /* DiskROM.cpp in Sources */,
|
||||
4B89451C201967B4007DE474 /* Disk.cpp in Sources */,
|
||||
4B302184208A550100773308 /* DiskII.cpp in Sources */,
|
||||
@@ -3840,6 +3861,7 @@
|
||||
4BD3A30B1EE755C800B5B501 /* Video.cpp in Sources */,
|
||||
4BBF99141C8FBA6F0075DAFB /* TextureBuilder.cpp in Sources */,
|
||||
4B5FADBA1DE3151600AEC565 /* FileHolder.cpp in Sources */,
|
||||
4BC891AD20F6EAB300EDE5B3 /* Rectangle.cpp in Sources */,
|
||||
4B643F3A1D77AD1900D431D6 /* CSStaticAnalyser.mm in Sources */,
|
||||
4B1497881EE4A1DA00CE2596 /* ZX80O81P.cpp in Sources */,
|
||||
4B894520201967B4007DE474 /* StaticAnalyser.cpp in Sources */,
|
||||
|
||||
@@ -111,6 +111,12 @@
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="aJh-i4-bef"/>
|
||||
<menuItem title="Save Screenshot" keyEquivalent="D" id="BVJ-oQ-hUp">
|
||||
<connections>
|
||||
<action selector="saveScreenshot:" target="-1" id="7ky-xD-tip"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="rXU-KX-GkZ"/>
|
||||
<menuItem title="Page Setup…" enabled="NO" keyEquivalent="P" id="qIS-W8-SiK">
|
||||
<modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
|
||||
<connections>
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.assets.pictures.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.bluetooth</key>
|
||||
<true/>
|
||||
<key>com.apple.security.device.usb</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
|
||||
#import "CSOpenGLView.h"
|
||||
#import "CSAudioQueue.h"
|
||||
|
||||
#import "CSBestEffortUpdater.h"
|
||||
#import "CSJoystickManager.h"
|
||||
|
||||
#include "KeyCodes.h"
|
||||
|
||||
@@ -9,4 +9,5 @@
|
||||
import Cocoa
|
||||
|
||||
class DocumentController: NSDocumentController {
|
||||
let joystickManager = CSJoystickManager()
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
// Copyright 2016 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import AudioToolbox
|
||||
import Cocoa
|
||||
|
||||
class MachineDocument:
|
||||
NSDocument,
|
||||
@@ -229,6 +229,13 @@ class MachineDocument:
|
||||
func windowDidResignKey(_ notification: Notification) {
|
||||
if let machine = self.machine {
|
||||
machine.clearAllKeys()
|
||||
machine.joystickManager = nil
|
||||
}
|
||||
}
|
||||
|
||||
func windowDidBecomeKey(_ notification: Notification) {
|
||||
if let machine = self.machine {
|
||||
machine.joystickManager = (DocumentController.shared as! DocumentController).joystickManager
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,6 +310,29 @@ class MachineDocument:
|
||||
return super.validateUserInterfaceItem(item)
|
||||
}
|
||||
|
||||
// Screenshot capture.
|
||||
@IBAction func saveScreenshot(_ sender: AnyObject!) {
|
||||
// Grab a date formatter and form a file name.
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .short
|
||||
dateFormatter.timeStyle = .long
|
||||
|
||||
let filename = ("Clock Signal Screen Shot " + dateFormatter.string(from: Date()) + ".png").replacingOccurrences(of: "/", with: "-")
|
||||
.replacingOccurrences(of: ":", with: ".")
|
||||
let pictursURL = FileManager.default.urls(for: .picturesDirectory, in: .userDomainMask)[0]
|
||||
let url = pictursURL.appendingPathComponent(filename)
|
||||
|
||||
// Obtain the machine's current display.
|
||||
var imageRepresentation: NSBitmapImageRep? = nil
|
||||
self.openGLView.perform {
|
||||
imageRepresentation = self.machine.imageRepresentation
|
||||
}
|
||||
|
||||
// Encode as a PNG and save.
|
||||
let pngData = imageRepresentation!.representation(using: .png, properties: [:])
|
||||
try! pngData?.write(to: url)
|
||||
}
|
||||
|
||||
// MARK: Activity display.
|
||||
class LED {
|
||||
let levelIndicator: NSLevelIndicator
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// CSJoystickManager.h
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 19/07/2018.
|
||||
// Copyright © 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/*!
|
||||
Models a single joystick button.
|
||||
Buttons have an index and are either currently pressed, or not.
|
||||
*/
|
||||
@interface CSJoystickButton: NSObject
|
||||
/// The button index. By convention the USB spec defines the first button as number 1.
|
||||
@property(nonatomic, readonly) NSInteger index;
|
||||
@property(nonatomic, readonly) bool isPressed;
|
||||
@end
|
||||
|
||||
typedef NS_ENUM(NSInteger, CSJoystickAxisType) {
|
||||
CSJoystickAxisTypeX,
|
||||
CSJoystickAxisTypeY,
|
||||
CSJoystickAxisTypeZ,
|
||||
};
|
||||
|
||||
/*!
|
||||
Models a joystick axis.
|
||||
Axes have a nominated type and a continuous value between 0 and 1.
|
||||
*/
|
||||
@interface CSJoystickAxis: NSObject
|
||||
@property(nonatomic, readonly) CSJoystickAxisType type;
|
||||
/// The current position of this axis in the range [0, 1].
|
||||
@property(nonatomic, readonly) float position;
|
||||
@end
|
||||
|
||||
typedef NS_OPTIONS(NSInteger, CSJoystickHatDirection) {
|
||||
CSJoystickHatDirectionUp = 1 << 0,
|
||||
CSJoystickHatDirectionDown = 1 << 1,
|
||||
CSJoystickHatDirectionLeft = 1 << 2,
|
||||
CSJoystickHatDirectionRight = 1 << 3,
|
||||
};
|
||||
|
||||
/*!
|
||||
Models a joystick hat.
|
||||
A hat is a digital directional input, so e.g. this is how thumbpads are represented.
|
||||
*/
|
||||
@interface CSJoystickHat: NSObject
|
||||
@property(nonatomic, readonly) CSJoystickHatDirection direction;
|
||||
@end
|
||||
|
||||
/*!
|
||||
Models a joystick.
|
||||
|
||||
A joystick is a collection of buttons, axes and hats, each of which holds a current
|
||||
state. The holder must use @c update to cause this joystick to read a fresh copy
|
||||
of its state.
|
||||
*/
|
||||
@interface CSJoystick: NSObject
|
||||
@property(nonatomic, readonly) NSArray<CSJoystickButton *> *buttons;
|
||||
@property(nonatomic, readonly) NSArray<CSJoystickAxis *> *axes;
|
||||
@property(nonatomic, readonly) NSArray<CSJoystickHat *> *hats;
|
||||
|
||||
- (void)update;
|
||||
@end
|
||||
|
||||
/*!
|
||||
The joystick manager watches for joystick connections and disconnections and
|
||||
offers a list of joysticks currently attached.
|
||||
|
||||
Be warned: this means using Apple's IOKit directly to watch for Bluetooth and
|
||||
USB HID devices. So to use this code, make sure you have USB and Bluetooth
|
||||
enabled for the app's sandbox.
|
||||
*/
|
||||
@interface CSJoystickManager : NSObject
|
||||
@property(nonatomic, readonly) NSArray<CSJoystick *> *joysticks;
|
||||
|
||||
/// Updates all joysticks.
|
||||
- (void)update;
|
||||
@end
|
||||
333
OSBindings/Mac/Clock Signal/Joystick Manager/CSJoystickManager.m
Normal file
333
OSBindings/Mac/Clock Signal/Joystick Manager/CSJoystickManager.m
Normal file
@@ -0,0 +1,333 @@
|
||||
//
|
||||
// CSJoystickManager.m
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 19/07/2018.
|
||||
// Copyright © 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#import "CSJoystickManager.h"
|
||||
|
||||
@import IOKit;
|
||||
#include <IOKit/hid/IOHIDLib.h>
|
||||
|
||||
#pragma mark - CSJoystickButton
|
||||
|
||||
@implementation CSJoystickButton {
|
||||
IOHIDElementRef _element;
|
||||
}
|
||||
|
||||
- (instancetype)initWithElement:(IOHIDElementRef)element index:(NSInteger)index {
|
||||
self = [super init];
|
||||
if(self) {
|
||||
_index = index;
|
||||
_element = (IOHIDElementRef)CFRetain(element);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
CFRelease(_element);
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString stringWithFormat:@"<CSJoystickButton: %p>; button %ld, %@", self, (long)self.index, self.isPressed ? @"pressed" : @"released"];
|
||||
}
|
||||
|
||||
- (IOHIDElementRef)element {
|
||||
return _element;
|
||||
}
|
||||
|
||||
- (void)setIsPressed:(bool)isPressed {
|
||||
_isPressed = isPressed;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - CSJoystickAxis
|
||||
|
||||
@implementation CSJoystickAxis {
|
||||
IOHIDElementRef _element;
|
||||
}
|
||||
|
||||
- (instancetype)initWithElement:(IOHIDElementRef)element type:(CSJoystickAxisType)type {
|
||||
self = [super init];
|
||||
if(self) {
|
||||
_element = (IOHIDElementRef)CFRetain(element);
|
||||
_type = type;
|
||||
_position = 0.5f;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
CFRelease(_element);
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString stringWithFormat:@"<CSJoystickAxis: %p>; type %d, value %0.2f", self, (int)self.type, self.position];
|
||||
}
|
||||
|
||||
- (IOHIDElementRef)element {
|
||||
return _element;
|
||||
}
|
||||
|
||||
- (void)setPosition:(float)position {
|
||||
_position = position;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - CSJoystickHat
|
||||
|
||||
@implementation CSJoystickHat {
|
||||
IOHIDElementRef _element;
|
||||
}
|
||||
|
||||
- (instancetype)initWithElement:(IOHIDElementRef)element {
|
||||
self = [super init];
|
||||
if(self) {
|
||||
_element = (IOHIDElementRef)CFRetain(element);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
CFRelease(_element);
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString stringWithFormat:@"<CSJoystickHat: %p>; direction %ld", self, (long)self.direction];
|
||||
}
|
||||
|
||||
- (IOHIDElementRef)element {
|
||||
return _element;
|
||||
}
|
||||
|
||||
- (void)setDirection:(CSJoystickHatDirection)direction {
|
||||
_direction = direction;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - CSJoystick
|
||||
|
||||
@implementation CSJoystick {
|
||||
IOHIDDeviceRef _device;
|
||||
}
|
||||
|
||||
- (instancetype)initWithButtons:(NSArray<CSJoystickButton *> *)buttons
|
||||
axes:(NSArray<CSJoystickAxis *> *)axes
|
||||
hats:(NSArray<CSJoystickHat *> *)hats
|
||||
device:(IOHIDDeviceRef)device {
|
||||
self = [super init];
|
||||
if(self) {
|
||||
// Sort buttons by index.
|
||||
_buttons = [buttons sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"index" ascending:YES]]];
|
||||
|
||||
// Sort axes by enum value.
|
||||
_axes = [axes sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"type" ascending:YES]]];
|
||||
|
||||
// Hats have no guaranteed ordering.
|
||||
_hats = hats;
|
||||
|
||||
// Keep hold of the device.
|
||||
_device = (IOHIDDeviceRef)CFRetain(device);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
CFRelease(_device);
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString stringWithFormat:@"<CSJoystick: %p>; buttons %@, axes %@, hats %@", self, self.buttons, self.axes, self.hats];
|
||||
}
|
||||
|
||||
- (void)update {
|
||||
// Update buttons.
|
||||
for(CSJoystickButton *button in _buttons) {
|
||||
IOHIDValueRef value;
|
||||
if(IOHIDDeviceGetValue(_device, button.element, &value) == kIOReturnSuccess) {
|
||||
// Some pressure-sensitive buttons return values greater than 1 for hard presses,
|
||||
// but this class rationalised everything to Boolean.
|
||||
button.isPressed = !!IOHIDValueGetIntegerValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Update hats.
|
||||
for(CSJoystickHat *hat in _hats) {
|
||||
IOHIDValueRef value;
|
||||
if(IOHIDDeviceGetValue(_device, hat.element, &value) == kIOReturnSuccess) {
|
||||
// Hats report a direction, which is either one of eight or one of four.
|
||||
CFIndex integerValue = IOHIDValueGetIntegerValue(value) - IOHIDElementGetLogicalMin(hat.element);
|
||||
const CFIndex range = 1 + IOHIDElementGetLogicalMax(hat.element) - IOHIDElementGetLogicalMin(hat.element);
|
||||
integerValue *= 8 / range;
|
||||
|
||||
// Map from the HID direction to the bit field.
|
||||
switch(integerValue) {
|
||||
default: hat.direction = 0; break;
|
||||
case 0: hat.direction = CSJoystickHatDirectionUp; break;
|
||||
case 1: hat.direction = CSJoystickHatDirectionUp | CSJoystickHatDirectionRight; break;
|
||||
case 2: hat.direction = CSJoystickHatDirectionRight; break;
|
||||
case 3: hat.direction = CSJoystickHatDirectionRight | CSJoystickHatDirectionDown; break;
|
||||
case 4: hat.direction = CSJoystickHatDirectionDown; break;
|
||||
case 5: hat.direction = CSJoystickHatDirectionDown | CSJoystickHatDirectionLeft; break;
|
||||
case 6: hat.direction = CSJoystickHatDirectionLeft; break;
|
||||
case 7: hat.direction = CSJoystickHatDirectionLeft | CSJoystickHatDirectionUp; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update axes.
|
||||
for(CSJoystickAxis *axis in _axes) {
|
||||
IOHIDValueRef value;
|
||||
if(IOHIDDeviceGetValue(_device, axis.element, &value) == kIOReturnSuccess) {
|
||||
const CFIndex integerValue = IOHIDValueGetIntegerValue(value) - IOHIDElementGetLogicalMin(axis.element);
|
||||
const CFIndex range = 1 + IOHIDElementGetLogicalMax(axis.element) - IOHIDElementGetLogicalMin(axis.element);
|
||||
axis.position = (float)integerValue / (float)range;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (IOHIDDeviceRef)device {
|
||||
return _device;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - CSJoystickManager
|
||||
|
||||
@interface CSJoystickManager ()
|
||||
- (void)deviceMatched:(IOHIDDeviceRef)device result:(IOReturn)result sender:(void *)sender;
|
||||
- (void)deviceRemoved:(IOHIDDeviceRef)device result:(IOReturn)result sender:(void *)sender;
|
||||
@end
|
||||
|
||||
static void DeviceMatched(void *context, IOReturn result, void *sender, IOHIDDeviceRef device) {
|
||||
[(__bridge CSJoystickManager *)context deviceMatched:device result:result sender:sender];
|
||||
}
|
||||
|
||||
static void DeviceRemoved(void *context, IOReturn result, void *sender, IOHIDDeviceRef device) {
|
||||
[(__bridge CSJoystickManager *)context deviceRemoved:device result:result sender:sender];
|
||||
}
|
||||
|
||||
@implementation CSJoystickManager {
|
||||
IOHIDManagerRef _hidManager;
|
||||
NSMutableArray<CSJoystick *> *_joysticks;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if(self) {
|
||||
_joysticks = [[NSMutableArray alloc] init];
|
||||
|
||||
_hidManager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
|
||||
if(!_hidManager) return nil;
|
||||
|
||||
NSArray<NSDictionary<NSString *, NSNumber *> *> *const multiple = @[
|
||||
@{ @kIOHIDDeviceUsagePageKey: @(kHIDPage_GenericDesktop), @kIOHIDDeviceUsageKey: @(kHIDUsage_GD_Joystick) },
|
||||
@{ @kIOHIDDeviceUsagePageKey: @(kHIDPage_GenericDesktop), @kIOHIDDeviceUsageKey: @(kHIDUsage_GD_GamePad) },
|
||||
@{ @kIOHIDDeviceUsagePageKey: @(kHIDPage_GenericDesktop), @kIOHIDDeviceUsageKey: @(kHIDUsage_GD_MultiAxisController) },
|
||||
];
|
||||
|
||||
IOHIDManagerSetDeviceMatchingMultiple(_hidManager, (__bridge CFArrayRef)multiple);
|
||||
IOHIDManagerRegisterDeviceMatchingCallback(_hidManager, DeviceMatched, (__bridge void *)self);
|
||||
IOHIDManagerRegisterDeviceRemovalCallback(_hidManager, DeviceRemoved, (__bridge void *)self);
|
||||
IOHIDManagerScheduleWithRunLoop(_hidManager, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
|
||||
|
||||
if(IOHIDManagerOpen(_hidManager, kIOHIDOptionsTypeNone) != kIOReturnSuccess) {
|
||||
NSLog(@"Failed to open HID manager");
|
||||
// something
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
IOHIDManagerUnscheduleFromRunLoop(_hidManager, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
|
||||
IOHIDManagerClose(_hidManager, kIOHIDOptionsTypeNone);
|
||||
CFRelease(_hidManager);
|
||||
}
|
||||
|
||||
- (void)deviceMatched:(IOHIDDeviceRef)device result:(IOReturn)result sender:(void *)sender {
|
||||
// Double check this joystick isn't already known.
|
||||
for(CSJoystick *joystick in _joysticks) {
|
||||
if(joystick.device == device) return;
|
||||
}
|
||||
|
||||
// Prepare to collate a list of buttons, axes and hats for the new device.
|
||||
NSMutableArray<CSJoystickButton *> *buttons = [[NSMutableArray alloc] init];
|
||||
NSMutableArray<CSJoystickAxis *> *axes = [[NSMutableArray alloc] init];
|
||||
NSMutableArray<CSJoystickHat *> *hats = [[NSMutableArray alloc] init];
|
||||
|
||||
// Inspect all elements for those that are comprehensible to this code.
|
||||
const CFArrayRef elements = IOHIDDeviceCopyMatchingElements(device, NULL, kIOHIDOptionsTypeNone);
|
||||
for(CFIndex index = 0; index < CFArrayGetCount(elements); ++index) {
|
||||
const IOHIDElementRef element = (IOHIDElementRef)CFArrayGetValueAtIndex(elements, index);
|
||||
|
||||
// Check that this element is either on the generic desktop page or else is a button.
|
||||
const uint32_t usagePage = IOHIDElementGetUsagePage(element);
|
||||
if(usagePage != kHIDPage_GenericDesktop && usagePage != kHIDPage_Button) continue;
|
||||
|
||||
// Then inspect the type.
|
||||
switch(IOHIDElementGetType(element)) {
|
||||
default: break;
|
||||
|
||||
case kIOHIDElementTypeInput_Button: {
|
||||
// Add a button; pretty easy stuff. 'Usage' provides a button index.
|
||||
const uint32_t usage = IOHIDElementGetUsage(element);
|
||||
[buttons addObject:[[CSJoystickButton alloc] initWithElement:element index:usage]];
|
||||
} break;
|
||||
|
||||
case kIOHIDElementTypeInput_Misc:
|
||||
case kIOHIDElementTypeInput_Axis: {
|
||||
CSJoystickAxisType axisType;
|
||||
switch(IOHIDElementGetUsage(element)) {
|
||||
default: continue;
|
||||
|
||||
// Three analogue axes are implemented here; there are another three sets
|
||||
// of these that could be parsed in the future if interesting.
|
||||
case kHIDUsage_GD_X: axisType = CSJoystickAxisTypeX; break;
|
||||
case kHIDUsage_GD_Y: axisType = CSJoystickAxisTypeY; break;
|
||||
case kHIDUsage_GD_Z: axisType = CSJoystickAxisTypeZ; break;
|
||||
|
||||
// A hatswitch is a multi-directional control all of its own.
|
||||
case kHIDUsage_GD_Hatswitch:
|
||||
[hats addObject:[[CSJoystickHat alloc] initWithElement:element]];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add the axis; if it was a hat switch or unrecognised then the code doesn't
|
||||
// reach here.
|
||||
[axes addObject:[[CSJoystickAxis alloc] initWithElement:element type:axisType]];
|
||||
} break;
|
||||
}
|
||||
}
|
||||
CFRelease(elements);
|
||||
|
||||
// Add this joystick to the list.
|
||||
[_joysticks addObject:[[CSJoystick alloc] initWithButtons:buttons axes:axes hats:hats device:device]];
|
||||
}
|
||||
|
||||
- (void)deviceRemoved:(IOHIDDeviceRef)device result:(IOReturn)result sender:(void *)sender {
|
||||
// If this joystick was recorded, remove it.
|
||||
for(CSJoystick *joystick in [_joysticks copy]) {
|
||||
if(joystick.device == device) {
|
||||
[_joysticks removeObject:joystick];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)update {
|
||||
[self.joysticks makeObjectsPerformSelector:@selector(update)];
|
||||
}
|
||||
|
||||
- (NSArray<CSJoystick *> *)joysticks {
|
||||
return [_joysticks copy];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -12,6 +12,7 @@
|
||||
#import "CSFastLoading.h"
|
||||
#import "CSOpenGLView.h"
|
||||
#import "CSStaticAnalyser.h"
|
||||
#import "CSJoystickManager.h"
|
||||
|
||||
@class CSMachine;
|
||||
@protocol CSMachineDelegate
|
||||
@@ -63,6 +64,7 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
|
||||
@property (nonatomic, readonly, nonnull) NSString *userDefaultsPrefix;
|
||||
|
||||
- (void)paste:(nonnull NSString *)string;
|
||||
@property (nonatomic, readonly, nonnull) NSBitmapImageRep *imageRepresentation;
|
||||
|
||||
@property (nonatomic, assign) BOOL useFastLoadingHack;
|
||||
@property (nonatomic, assign) CSMachineVideoSignal videoSignal;
|
||||
@@ -74,6 +76,7 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
|
||||
@property (nonatomic, readonly) BOOL hasKeyboard;
|
||||
@property (nonatomic, readonly) BOOL hasJoystick;
|
||||
@property (nonatomic, assign) CSMachineKeyboardInputMode inputMode;
|
||||
@property (nonatomic, nullable) CSJoystickManager *joystickManager;
|
||||
|
||||
// LED list.
|
||||
@property (nonatomic, readonly, nonnull) NSArray<NSString *> *leds;
|
||||
|
||||
@@ -57,9 +57,6 @@ struct ActivityObserver: public Activity::Observer {
|
||||
[machine addLED:[NSString stringWithUTF8String:name.c_str()]];
|
||||
}
|
||||
|
||||
void register_drive(const std::string &name) override {
|
||||
}
|
||||
|
||||
void set_led_status(const std::string &name, bool lit) override {
|
||||
[machine.delegate machine:machine led:[NSString stringWithUTF8String:name.c_str()] didChangeToLit:lit];
|
||||
}
|
||||
@@ -68,9 +65,6 @@ struct ActivityObserver: public Activity::Observer {
|
||||
[machine.delegate machine:machine ledShouldBlink:[NSString stringWithUTF8String:name.c_str()]];
|
||||
}
|
||||
|
||||
void set_drive_motor_status(const std::string &name, bool is_on) override {
|
||||
}
|
||||
|
||||
__unsafe_unretained CSMachine *machine;
|
||||
};
|
||||
|
||||
@@ -81,7 +75,9 @@ struct ActivityObserver: public Activity::Observer {
|
||||
|
||||
CSStaticAnalyser *_analyser;
|
||||
std::unique_ptr<Machine::DynamicMachine> _machine;
|
||||
JoystickMachine::Machine *_joystickMachine;
|
||||
|
||||
CSJoystickManager *_joystickManager;
|
||||
std::bitset<65536> _depressedKeys;
|
||||
NSMutableArray<NSString *> *_leds;
|
||||
}
|
||||
@@ -108,6 +104,8 @@ struct ActivityObserver: public Activity::Observer {
|
||||
|
||||
_speakerDelegate.machine = self;
|
||||
_speakerDelegate.machineAccessLock = _delegateMachineAccessLock;
|
||||
|
||||
_joystickMachine = _machine->joystick_machine();
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -168,6 +166,54 @@ struct ActivityObserver: public Activity::Observer {
|
||||
|
||||
- (void)runForInterval:(NSTimeInterval)interval {
|
||||
@synchronized(self) {
|
||||
if(_joystickMachine && _joystickManager) {
|
||||
[_joystickManager update];
|
||||
|
||||
// TODO: configurable mapping from physical joypad inputs to machine inputs.
|
||||
// Until then, apply a default mapping.
|
||||
|
||||
size_t c = 0;
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> &machine_joysticks = _joystickMachine->get_joysticks();
|
||||
for(CSJoystick *joystick in _joystickManager.joysticks) {
|
||||
size_t target = c % machine_joysticks.size();
|
||||
++++c;
|
||||
|
||||
// Post the first two analogue axes presented by the controller as horizontal and vertical inputs,
|
||||
// unless the user seems to be using a hat.
|
||||
// SDL will return a value in the range [-32768, 32767], so map from that to [0, 1.0]
|
||||
if(!joystick.hats.count || !joystick.hats[0].direction) {
|
||||
if(joystick.axes.count > 0) {
|
||||
const float x_axis = joystick.axes[0].position;
|
||||
machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Horizontal), x_axis);
|
||||
}
|
||||
if(joystick.axes.count > 1) {
|
||||
const float y_axis = joystick.axes[1].position;
|
||||
machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Vertical), y_axis);
|
||||
}
|
||||
} else {
|
||||
// Forward hats as directions; hats always override analogue inputs.
|
||||
for(CSJoystickHat *hat in joystick.hats) {
|
||||
machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Up), !!(hat.direction & CSJoystickHatDirectionUp));
|
||||
machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Down), !!(hat.direction & CSJoystickHatDirectionDown));
|
||||
machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Left), !!(hat.direction & CSJoystickHatDirectionLeft));
|
||||
machine_joysticks[target]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Right), !!(hat.direction & CSJoystickHatDirectionRight));
|
||||
}
|
||||
}
|
||||
|
||||
// Forward all fire buttons, mapping as a function of index.
|
||||
if(machine_joysticks[target]->get_number_of_fire_buttons()) {
|
||||
std::vector<bool> button_states((size_t)machine_joysticks[target]->get_number_of_fire_buttons());
|
||||
for(CSJoystickButton *button in joystick.buttons) {
|
||||
if(button.isPressed) button_states[(size_t)(((int)button.index - 1) % machine_joysticks[target]->get_number_of_fire_buttons())] = true;
|
||||
}
|
||||
for(size_t index = 0; index < button_states.size(); ++index) {
|
||||
machine_joysticks[target]->set_input(
|
||||
Inputs::Joystick::Input(Inputs::Joystick::Input::Type::Fire, index),
|
||||
button_states[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_machine->crt_machine()->run_for(interval);
|
||||
}
|
||||
}
|
||||
@@ -197,6 +243,43 @@ struct ActivityObserver: public Activity::Observer {
|
||||
keyboardMachine->type_string([paste UTF8String]);
|
||||
}
|
||||
|
||||
- (NSBitmapImageRep *)imageRepresentation {
|
||||
// Get the current viewport to establish framebuffer size. Then determine how wide the
|
||||
// centre 4/3 of that would be.
|
||||
GLint dimensions[4];
|
||||
glGetIntegerv(GL_VIEWPORT, dimensions);
|
||||
GLint proportionalWidth = (dimensions[3] * 4) / 3;
|
||||
|
||||
// Grab the framebuffer contents.
|
||||
std::vector<uint8_t> temporaryData(static_cast<size_t>(proportionalWidth * dimensions[3] * 3));
|
||||
glReadPixels((dimensions[2] - proportionalWidth) >> 1, 0, proportionalWidth, dimensions[3], GL_RGB, GL_UNSIGNED_BYTE, temporaryData.data());
|
||||
|
||||
// Generate an NSBitmapImageRep and populate it with a vertical flip
|
||||
// of the original data.
|
||||
NSBitmapImageRep *const result =
|
||||
[[NSBitmapImageRep alloc]
|
||||
initWithBitmapDataPlanes:NULL
|
||||
pixelsWide:proportionalWidth
|
||||
pixelsHigh:dimensions[3]
|
||||
bitsPerSample:8
|
||||
samplesPerPixel:3
|
||||
hasAlpha:NO
|
||||
isPlanar:NO
|
||||
colorSpaceName:NSDeviceRGBColorSpace
|
||||
bytesPerRow:3 * proportionalWidth
|
||||
bitsPerPixel:0];
|
||||
|
||||
const size_t line_size = static_cast<size_t>(proportionalWidth * 3);
|
||||
for(GLint y = 0; y < dimensions[3]; ++y) {
|
||||
memcpy(
|
||||
&result.bitmapData[static_cast<size_t>(y) * line_size],
|
||||
&temporaryData[static_cast<size_t>(dimensions[3] - y - 1) * line_size],
|
||||
line_size);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)applyMedia:(const Analyser::Static::Media &)media {
|
||||
@synchronized(self) {
|
||||
MediaTarget::Machine *const mediaTarget = _machine->media_target();
|
||||
@@ -204,6 +287,18 @@ struct ActivityObserver: public Activity::Observer {
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setJoystickManager:(CSJoystickManager *)joystickManager {
|
||||
@synchronized(self) {
|
||||
_joystickManager = joystickManager;
|
||||
if(_joystickMachine) {
|
||||
std::vector<std::unique_ptr<Inputs::Joystick>> &machine_joysticks = _joystickMachine->get_joysticks();
|
||||
for(const auto &joystick: machine_joysticks) {
|
||||
joystick->reset_all_inputs();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setKey:(uint16_t)key characters:(NSString *)characters isPressed:(BOOL)isPressed {
|
||||
auto keyboard_machine = _machine->keyboard_machine();
|
||||
if(self.inputMode == CSMachineKeyboardInputModeKeyboard && keyboard_machine) {
|
||||
@@ -300,7 +395,7 @@ struct ActivityObserver: public Activity::Observer {
|
||||
case VK_ANSI_D: joysticks[0]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Fire, 2), is_pressed); break;
|
||||
case VK_ANSI_F: joysticks[0]->set_input(Inputs::Joystick::Input(Inputs::Joystick::Input::Fire, 3), is_pressed); break;
|
||||
default:
|
||||
if(characters) {
|
||||
if(characters.length) {
|
||||
joysticks[0]->set_input(Inputs::Joystick::Input([characters characterAtIndex:0]), is_pressed);
|
||||
} else {
|
||||
joysticks[0]->set_input(Inputs::Joystick::Input::Fire, is_pressed);
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
|
||||
typedef NS_ENUM(NSInteger, CSMachineAppleIIModel) {
|
||||
CSMachineAppleIIModelAppleII,
|
||||
CSMachineAppleIIModelAppleIIPlus
|
||||
CSMachineAppleIIModelAppleIIPlus,
|
||||
CSMachineAppleIIModelAppleIIe
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSInteger, CSMachineAppleIIDiskController) {
|
||||
|
||||
@@ -168,7 +168,11 @@ static Analyser::Static::ZX8081::Target::MemoryModel ZX8081MemoryModelFromSize(K
|
||||
using Target = Analyser::Static::AppleII::Target;
|
||||
std::unique_ptr<Target> target(new Target);
|
||||
target->machine = Analyser::Machine::AppleII;
|
||||
target->model = (model == CSMachineAppleIIModelAppleII) ? Target::Model::II : Target::Model::IIplus;
|
||||
switch(model) {
|
||||
default: target->model = Target::Model::II; break;
|
||||
case CSMachineAppleIIModelAppleIIPlus: target->model = Target::Model::IIplus; break;
|
||||
case CSMachineAppleIIModelAppleIIe: target->model = Target::Model::IIe; break;
|
||||
}
|
||||
switch(diskController) {
|
||||
default:
|
||||
case CSMachineAppleIIDiskControllerNone: target->disk_controller = Target::DiskController::None; break;
|
||||
@@ -184,7 +188,7 @@ static Analyser::Static::ZX8081::Target::MemoryModel ZX8081MemoryModelFromSize(K
|
||||
- (NSString *)optionsPanelNibName {
|
||||
switch(_targets.front()->machine) {
|
||||
case Analyser::Machine::AmstradCPC: return @"CompositeOptions";
|
||||
case Analyser::Machine::AppleII: return @"AppleIIOptions";
|
||||
// case Analyser::Machine::AppleII: return @"AppleIIOptions";
|
||||
case Analyser::Machine::Atari2600: return @"Atari2600Options";
|
||||
case Analyser::Machine::Electron: return @"QuickLoadCompositeOptions";
|
||||
case Analyser::Machine::MSX: return @"QuickLoadCompositeOptions";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14109" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14113" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14109"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14113"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@@ -64,11 +64,11 @@ Gw
|
||||
<tabViewItems>
|
||||
<tabViewItem label="Apple II" identifier="appleii" id="P59-QG-LOa">
|
||||
<view key="view" id="dHz-Yv-GNq">
|
||||
<rect key="frame" x="10" y="33" width="554" height="93"/>
|
||||
<rect key="frame" x="10" y="33" width="554" height="94"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="V5Z-dX-Ns4">
|
||||
<rect key="frame" x="15" y="71" width="46" height="17"/>
|
||||
<rect key="frame" x="15" y="72" width="46" height="17"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Model:" id="qV3-2P-3JW">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -76,7 +76,7 @@ Gw
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="WnO-ef-IC6">
|
||||
<rect key="frame" x="15" y="40" width="96" height="17"/>
|
||||
<rect key="frame" x="15" y="41" width="96" height="17"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Disk controller:" id="kbf-rc-Y4M">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -84,7 +84,7 @@ Gw
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="jli-ac-Sij">
|
||||
<rect key="frame" x="65" y="66" width="91" height="26"/>
|
||||
<rect key="frame" x="65" y="67" width="91" height="26"/>
|
||||
<popUpButtonCell key="cell" type="push" title="Apple II" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="VBQ-JG-AeM" id="U6V-us-O2F">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
@@ -92,12 +92,13 @@ Gw
|
||||
<items>
|
||||
<menuItem title="Apple II" state="on" id="VBQ-JG-AeM"/>
|
||||
<menuItem title="Apple II+" tag="1" id="Yme-Wn-Obh"/>
|
||||
<menuItem title="Apple IIe" tag="2" id="AMt-WU-a0H"/>
|
||||
</items>
|
||||
</menu>
|
||||
</popUpButtonCell>
|
||||
</popUpButton>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="LSB-WP-FMi">
|
||||
<rect key="frame" x="115" y="35" width="132" height="26"/>
|
||||
<rect key="frame" x="115" y="36" width="132" height="26"/>
|
||||
<popUpButtonCell key="cell" type="push" title="Sixteen Sector" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="16" imageScaling="proportionallyDown" inset="2" selectedItem="QaV-Yr-k9o" id="8BT-RV-2Nm">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
@@ -128,11 +129,11 @@ Gw
|
||||
</tabViewItem>
|
||||
<tabViewItem label="Amstrad CPC" identifier="cpc" id="JmB-OF-xcM">
|
||||
<view key="view" id="5zS-Nj-Ynx">
|
||||
<rect key="frame" x="10" y="33" width="554" height="99"/>
|
||||
<rect key="frame" x="10" y="33" width="554" height="94"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="00d-sg-Krh">
|
||||
<rect key="frame" x="65" y="72" width="94" height="26"/>
|
||||
<rect key="frame" x="65" y="67" width="94" height="26"/>
|
||||
<popUpButtonCell key="cell" type="push" title="CPC6128" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="6128" imageScaling="proportionallyDown" inset="2" selectedItem="klh-ZE-Agp" id="hVJ-h6-iea">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
@@ -146,7 +147,7 @@ Gw
|
||||
</popUpButtonCell>
|
||||
</popUpButton>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="q9q-sl-J0q">
|
||||
<rect key="frame" x="15" y="77" width="46" height="17"/>
|
||||
<rect key="frame" x="15" y="72" width="46" height="17"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Model:" id="Cw3-q5-1bC">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
|
||||
@@ -130,6 +130,7 @@ class MachinePicker: NSObject {
|
||||
var model: CSMachineAppleIIModel = .appleII
|
||||
switch appleIIModelButton!.selectedTag() {
|
||||
case 1: model = .appleIIPlus
|
||||
case 2: model = .appleIIe
|
||||
case 0: fallthrough
|
||||
default: model = .appleII
|
||||
}
|
||||
|
||||
@@ -6,10 +6,14 @@
|
||||
// Copyright 2017 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
@@ -21,6 +25,9 @@
|
||||
|
||||
#include "../../Concurrency/BestEffortUpdater.hpp"
|
||||
|
||||
#include "../../Activity/Observer.hpp"
|
||||
#include "../../Outputs/CRT/Internals/Rectangle.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
struct BestEffortUpdaterDelegate: public Concurrency::BestEffortUpdater::Delegate {
|
||||
@@ -31,8 +38,8 @@ struct BestEffortUpdaterDelegate: public Concurrency::BestEffortUpdater::Delegat
|
||||
Machine::DynamicMachine *machine;
|
||||
};
|
||||
|
||||
// This is set to a relatively large number for now.
|
||||
struct SpeakerDelegate: public Outputs::Speaker::Speaker::Delegate {
|
||||
// This is set to a relatively large number for now.
|
||||
static const int buffer_size = 1024;
|
||||
|
||||
void speaker_did_complete_samples(Outputs::Speaker::Speaker *speaker, const std::vector<int16_t> &buffer) override {
|
||||
@@ -69,6 +76,89 @@ struct SpeakerDelegate: public Outputs::Speaker::Speaker::Delegate {
|
||||
std::vector<int16_t> audio_buffer_;
|
||||
};
|
||||
|
||||
class ActivityObserver: public Activity::Observer {
|
||||
public:
|
||||
ActivityObserver(Activity::Source *source, float aspect_ratio) {
|
||||
// Get the suorce to supply all LEDs and drives.
|
||||
source->set_activity_observer(this);
|
||||
|
||||
// The objective is to display drives on one side of the screen, other LEDs on the other. Drives
|
||||
// may or may not have LEDs and this code intends to display only those which do; so a quick
|
||||
// comparative processing of the two lists is called for.
|
||||
|
||||
// Strip the list of drives to only those which have LEDs. Thwy're the ones that'll be displayed.
|
||||
drives_.resize(std::remove_if(drives_.begin(), drives_.end(), [this](const std::string &string) {
|
||||
return std::find(leds_.begin(), leds_.end(), string) == leds_.end();
|
||||
}) - drives_.begin());
|
||||
|
||||
// Remove from the list of LEDs any which are drives. Those will be represented separately.
|
||||
leds_.resize(std::remove_if(leds_.begin(), leds_.end(), [this](const std::string &string) {
|
||||
return std::find(drives_.begin(), drives_.end(), string) != drives_.end();
|
||||
}) - leds_.begin());
|
||||
|
||||
set_aspect_ratio(aspect_ratio);
|
||||
}
|
||||
|
||||
void set_aspect_ratio(float aspect_ratio) {
|
||||
lights_.clear();
|
||||
|
||||
// Generate a bunch of LEDs for connected drives.
|
||||
const float height = 0.05f;
|
||||
const float width = height / aspect_ratio;
|
||||
const float right_x = 1.0f - 2.0f * width;
|
||||
float y = 1.0f - 2.0f * height;
|
||||
for(const auto &drive: drives_) {
|
||||
// TODO: use std::make_unique as below, if/when formally embracing C++14.
|
||||
lights_.emplace(std::make_pair(drive, std::unique_ptr<OpenGL::Rectangle>(new OpenGL::Rectangle(right_x, y, width, height))));
|
||||
y -= height * 2.0f;
|
||||
}
|
||||
|
||||
/*
|
||||
This would generate LEDs for things other than drives; I'm declining for now
|
||||
due to the inexpressiveness of just painting a rectangle.
|
||||
|
||||
const float left_x = -1.0f + 2.0f * width;
|
||||
y = 1.0f - 2.0f * height;
|
||||
for(const auto &led: leds_) {
|
||||
lights_.emplace(std::make_pair(led, std::make_unique<OpenGL::Rectangle>(left_x, y, width, height)));
|
||||
y -= height * 2.0f;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
void draw() {
|
||||
for(const auto &lit_led: lit_leds_) {
|
||||
if(blinking_leds_.find(lit_led) == blinking_leds_.end() && lights_.find(lit_led) != lights_.end())
|
||||
lights_[lit_led]->draw(0.0, 0.8, 0.0);
|
||||
}
|
||||
blinking_leds_.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::string> leds_;
|
||||
void register_led(const std::string &name) override {
|
||||
leds_.push_back(name);
|
||||
}
|
||||
|
||||
std::vector<std::string> drives_;
|
||||
void register_drive(const std::string &name) override {
|
||||
drives_.push_back(name);
|
||||
}
|
||||
|
||||
void set_led_status(const std::string &name, bool lit) override {
|
||||
if(lit) lit_leds_.insert(name);
|
||||
else lit_leds_.erase(name);
|
||||
}
|
||||
|
||||
void announce_drive_event(const std::string &name, DriveEvent event) override {
|
||||
blinking_leds_.insert(name);
|
||||
}
|
||||
|
||||
std::map<std::string, std::unique_ptr<OpenGL::Rectangle>> lights_;
|
||||
std::set<std::string> lit_leds_;
|
||||
std::set<std::string> blinking_leds_;
|
||||
};
|
||||
|
||||
bool KeyboardKeyForSDLScancode(SDL_Keycode scancode, Inputs::Keyboard::Key &key) {
|
||||
#define BIND(x, y) case SDL_SCANCODE_##x: key = Inputs::Keyboard::Key::y; break;
|
||||
switch(scancode) {
|
||||
@@ -146,7 +236,7 @@ ParsedArguments parse_arguments(int argc, char *argv[]) {
|
||||
// --flag sets a Boolean option to true.
|
||||
// --flag=value sets the value for a list option.
|
||||
// name sets the file name to load.
|
||||
|
||||
|
||||
// Anything starting with a dash always makes a selection; otherwise it's a file name.
|
||||
if(arg[0] == '-') {
|
||||
while(*arg == '-') arg++;
|
||||
@@ -178,7 +268,7 @@ std::string final_path_component(const std::string &path) {
|
||||
|
||||
// Find the last slash...
|
||||
auto final_slash = path.find_last_of("/\\");
|
||||
|
||||
|
||||
// If no slash was found at all, return the whole path.
|
||||
if(final_slash == std::string::npos) {
|
||||
return path;
|
||||
@@ -193,6 +283,22 @@ std::string final_path_component(const std::string &path) {
|
||||
return path.substr(final_slash+1, path.size() - final_slash - 1);
|
||||
}
|
||||
|
||||
/*!
|
||||
Executes @c command and returns its STDOUT.
|
||||
*/
|
||||
std::string system_get(const char *command) {
|
||||
std::unique_ptr<FILE, decltype((pclose))> pipe(popen(command, "r"), pclose);
|
||||
if(!pipe) return "";
|
||||
|
||||
std::string result;
|
||||
while(!feof(pipe.get())) {
|
||||
std::array<char, 256> buffer;
|
||||
if (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr)
|
||||
result += buffer.data();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
@@ -215,7 +321,7 @@ int main(int argc, char *argv[]) {
|
||||
std::cout << machine_options.first << ":" << std::endl;
|
||||
for(const auto &option: machine_options.second) {
|
||||
std::cout << '\t' << "--" << option->short_name;
|
||||
|
||||
|
||||
Configurable::ListOption *list_option = dynamic_cast<Configurable::ListOption *>(option.get());
|
||||
if(list_option) {
|
||||
std::cout << "={";
|
||||
@@ -394,7 +500,7 @@ int main(int argc, char *argv[]) {
|
||||
if(configurable_device) {
|
||||
// Establish user-friendly options by default.
|
||||
configurable_device->set_selections(configurable_device->get_user_friendly_selections());
|
||||
|
||||
|
||||
// Consider transcoding any list selections that map to Boolean options.
|
||||
for(const auto &option: configurable_device->get_options()) {
|
||||
// Check for a corresponding selection.
|
||||
@@ -462,6 +568,16 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
If the machine offers anything for activity observation,
|
||||
create and register an activity observer.
|
||||
*/
|
||||
std::unique_ptr<ActivityObserver> activity_observer;
|
||||
Activity::Source *const activity_source = machine->activity_source();
|
||||
if(activity_source) {
|
||||
activity_observer.reset(new ActivityObserver(activity_source, 4.0f / 3.0f));
|
||||
}
|
||||
|
||||
// Run the main event loop until the OS tells us to quit.
|
||||
bool should_quit = false;
|
||||
Uint32 fullscreen_mode = 0;
|
||||
@@ -479,6 +595,7 @@ int main(int argc, char *argv[]) {
|
||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &target_framebuffer);
|
||||
machine->crt_machine()->get_crt()->set_target_framebuffer(target_framebuffer);
|
||||
SDL_GetWindowSize(window, &window_width, &window_height);
|
||||
if(activity_observer) activity_observer->set_aspect_ratio(static_cast<float>(window_width) / static_cast<float>(window_height));
|
||||
} break;
|
||||
|
||||
default: break;
|
||||
@@ -500,6 +617,63 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
// Capture ctrl+shift+d as a take-a-screenshot command.
|
||||
if(event.key.keysym.sym == SDLK_d && (SDL_GetModState()&KMOD_CTRL) && (SDL_GetModState()&KMOD_SHIFT)) {
|
||||
// Pick a width to capture that will preserve a 4:3 output aspect ratio.
|
||||
const int proportional_width = (window_height * 4) / 3;
|
||||
|
||||
// Grab the screen buffer.
|
||||
std::vector<uint8_t> pixels(proportional_width * window_height * 4);
|
||||
glReadPixels((window_width - proportional_width) >> 1, 0, proportional_width, window_height, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||
|
||||
// Flip the buffer vertically, because SDL and OpenGL do not agree about
|
||||
// the basis axes.
|
||||
std::vector<uint8_t> swap_buffer(proportional_width*4);
|
||||
for(int y = 0; y < window_height >> 1; ++y) {
|
||||
memcpy(swap_buffer.data(), &pixels[y*proportional_width*4], swap_buffer.size());
|
||||
memcpy(&pixels[y*proportional_width*4], &pixels[(window_height - 1 - y)*proportional_width*4], swap_buffer.size());
|
||||
memcpy(&pixels[(window_height - 1 - y)*proportional_width*4], swap_buffer.data(), swap_buffer.size());
|
||||
}
|
||||
|
||||
// Pick the directory for images. Try `xdg-user-dir PICTURES` first.
|
||||
std::string target_directory = system_get("xdg-user-dir PICTURES");
|
||||
|
||||
// Make sure there are no newlines.
|
||||
target_directory.erase(std::remove(target_directory.begin(), target_directory.end(), '\n'), target_directory.end());
|
||||
target_directory.erase(std::remove(target_directory.begin(), target_directory.end(), '\r'), target_directory.end());
|
||||
|
||||
// Fall back on the HOME directory if necessary.
|
||||
if(target_directory.empty()) target_directory = getenv("HOME");
|
||||
|
||||
// Find the first available name of the form ~/clk-screenshot-<number>.bmp.
|
||||
size_t index = 0;
|
||||
std::string target;
|
||||
while(true) {
|
||||
target = target_directory + "/clk-screenshot-" + std::to_string(index) + ".bmp";
|
||||
|
||||
struct stat file_stats;
|
||||
if(stat(target.c_str(), &file_stats))
|
||||
break;
|
||||
|
||||
++index;
|
||||
}
|
||||
|
||||
// Create a suitable SDL surface and save the thing.
|
||||
const bool is_big_endian = SDL_BYTEORDER == SDL_BIG_ENDIAN;
|
||||
SDL_Surface *const surface = SDL_CreateRGBSurfaceFrom(
|
||||
pixels.data(),
|
||||
proportional_width, window_height,
|
||||
8*4,
|
||||
proportional_width*4,
|
||||
is_big_endian ? 0xff000000 : 0x000000ff,
|
||||
is_big_endian ? 0x00ff0000 : 0x0000ff00,
|
||||
is_big_endian ? 0x0000ff00 : 0x00ff0000,
|
||||
0);
|
||||
SDL_SaveBMP(surface, target.c_str());
|
||||
SDL_FreeSurface(surface);
|
||||
break;
|
||||
}
|
||||
|
||||
// deliberate fallthrough...
|
||||
case SDL_KEYUP: {
|
||||
|
||||
@@ -610,6 +784,7 @@ int main(int argc, char *argv[]) {
|
||||
// Display a new frame and wait for vsync.
|
||||
updater.update();
|
||||
machine->crt_machine()->get_crt()->draw_frame(static_cast<unsigned int>(window_width), static_cast<unsigned int>(window_height), false);
|
||||
if(activity_observer) activity_observer->draw();
|
||||
SDL_GL_SwapWindow(window);
|
||||
}
|
||||
|
||||
|
||||
@@ -227,11 +227,18 @@ void OpenGLOutputBuilder::draw_frame(unsigned int output_width, unsigned int out
|
||||
output_shader_program_->set_output_size(output_width, output_height, visible_area_);
|
||||
last_output_width_ = output_width;
|
||||
last_output_height_ = output_height;
|
||||
|
||||
// Configure a right gutter to crop the right-hand 2% of the display.
|
||||
right_overlay_.reset(new OpenGL::Rectangle(output_shader_program_->get_right_extent() * 0.98f, -1.0f, 1.0f, 2.0f));
|
||||
}
|
||||
output_shader_program_->bind();
|
||||
|
||||
// draw
|
||||
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, (GLsizei)array_submission.output_size / OutputVertexSize);
|
||||
|
||||
// mask off the gutter
|
||||
glDisable(GL_BLEND);
|
||||
right_overlay_->draw(0.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
#ifdef GL_NV_texture_barrier
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
#include "Shaders/OutputShader.hpp"
|
||||
#include "Shaders/IntermediateShader.hpp"
|
||||
#include "Rectangle.hpp"
|
||||
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
@@ -106,6 +107,18 @@ class OpenGLOutputBuilder {
|
||||
|
||||
float integer_coordinate_multiplier_ = 1.0f;
|
||||
|
||||
// Maintain a couple of rectangles for masking off the extreme edge of the display;
|
||||
// this is a bit of a cheat: there's some tolerance in when a sync pulse will be
|
||||
// generated. So it might be slightly later than expected. Which might cause a scan
|
||||
// that is slightly longer than expected. Which means that from then on, those scans
|
||||
// might have touched parts of the extreme edge of the display which are not rescanned.
|
||||
// Which because I've implemented persistence-of-vision as an in-buffer effect will
|
||||
// cause perpetual persistence.
|
||||
//
|
||||
// The fix: just always treat that area as invisible. This is acceptable thanks to
|
||||
// the concept of overscan. One is allowed not to display extreme ends of the image.
|
||||
std::unique_ptr<OpenGL::Rectangle> right_overlay_;
|
||||
|
||||
public:
|
||||
// These two are protected by output_mutex_.
|
||||
TextureBuilder texture_builder;
|
||||
@@ -151,7 +164,7 @@ class OpenGLOutputBuilder {
|
||||
if(!composite_output_buffer_is_full())
|
||||
composite_src_output_y_++;
|
||||
}
|
||||
|
||||
|
||||
void set_target_framebuffer(GLint);
|
||||
void draw_frame(unsigned int output_width, unsigned int output_height, bool only_if_dirty);
|
||||
void set_openGL_context_will_change(bool should_delete_resources);
|
||||
|
||||
74
Outputs/CRT/Internals/Rectangle.cpp
Normal file
74
Outputs/CRT/Internals/Rectangle.cpp
Normal file
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// Rectangle.cpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 11/07/2018.
|
||||
// Copyright © 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Rectangle.hpp"
|
||||
|
||||
using namespace OpenGL;
|
||||
|
||||
Rectangle::Rectangle(float x, float y, float width, float height):
|
||||
pixel_shader_(
|
||||
"#version 150\n"
|
||||
|
||||
"in vec2 position;"
|
||||
|
||||
"void main(void)"
|
||||
"{"
|
||||
"gl_Position = vec4(position, 0.0, 1.0);"
|
||||
"}",
|
||||
|
||||
"#version 150\n"
|
||||
|
||||
"uniform vec4 colour;"
|
||||
"out vec4 fragColour;"
|
||||
|
||||
"void main(void)"
|
||||
"{"
|
||||
"fragColour = colour;"
|
||||
"}"
|
||||
){
|
||||
pixel_shader_.bind();
|
||||
|
||||
glGenVertexArrays(1, &drawing_vertex_array_);
|
||||
glGenBuffers(1, &drawing_array_buffer_);
|
||||
|
||||
glBindVertexArray(drawing_vertex_array_);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, drawing_array_buffer_);
|
||||
|
||||
GLint position_attribute = pixel_shader_.get_attrib_location("position");
|
||||
glEnableVertexAttribArray(static_cast<GLuint>(position_attribute));
|
||||
|
||||
glVertexAttribPointer(
|
||||
(GLuint)position_attribute,
|
||||
2,
|
||||
GL_FLOAT,
|
||||
GL_FALSE,
|
||||
2 * sizeof(GLfloat),
|
||||
(void *)0);
|
||||
|
||||
colour_uniform_ = pixel_shader_.get_uniform_location("colour");
|
||||
|
||||
float buffer[4*2];
|
||||
|
||||
// Store positions.
|
||||
buffer[0] = x; buffer[1] = y;
|
||||
buffer[2] = x; buffer[3] = y + height;
|
||||
buffer[4] = x + width; buffer[5] = y;
|
||||
buffer[6] = x + width; buffer[7] = y + height;
|
||||
|
||||
// Upload buffer.
|
||||
glBindBuffer(GL_ARRAY_BUFFER, drawing_array_buffer_);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof(buffer), buffer, GL_STATIC_DRAW);
|
||||
}
|
||||
|
||||
void Rectangle::draw(float red, float green, float blue) {
|
||||
pixel_shader_.bind();
|
||||
glUniform4f(colour_uniform_, red, green, blue, 1.0);
|
||||
|
||||
glBindVertexArray(drawing_vertex_array_);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
}
|
||||
41
Outputs/CRT/Internals/Rectangle.hpp
Normal file
41
Outputs/CRT/Internals/Rectangle.hpp
Normal file
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// Rectangle.hpp
|
||||
// Clock Signal
|
||||
//
|
||||
// Created by Thomas Harte on 11/07/2018.
|
||||
// Copyright © 2018 Thomas Harte. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Rectangle_hpp
|
||||
#define Rectangle_hpp
|
||||
|
||||
#include "OpenGL.hpp"
|
||||
#include "Shaders/Shader.hpp"
|
||||
#include <memory>
|
||||
|
||||
namespace OpenGL {
|
||||
|
||||
/*!
|
||||
Provides a wrapper for drawing a solid, single-colour rectangle.
|
||||
*/
|
||||
class Rectangle {
|
||||
public:
|
||||
/*!
|
||||
Instantiates an instance of Rectange with the coordinates given.
|
||||
*/
|
||||
Rectangle(float x, float y, float width, float height);
|
||||
|
||||
/*!
|
||||
Draws this rectangle in the colour supplied.
|
||||
*/
|
||||
void draw(float red, float green, float blue);
|
||||
|
||||
private:
|
||||
Shader pixel_shader_;
|
||||
GLuint drawing_vertex_array_ = 0, drawing_array_buffer_ = 0;
|
||||
GLint colour_uniform_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif /* Rectangle_hpp */
|
||||
@@ -19,7 +19,7 @@ namespace OpenGL {
|
||||
class IntermediateShader: public Shader {
|
||||
public:
|
||||
using Shader::Shader;
|
||||
|
||||
|
||||
enum class Input {
|
||||
/// Contains the 2d start position of this run's input data.
|
||||
InputStart,
|
||||
|
||||
@@ -94,15 +94,21 @@ std::unique_ptr<OutputShader> OutputShader::make_shader(const char *fragment_met
|
||||
|
||||
void OutputShader::set_output_size(unsigned int output_width, unsigned int output_height, Outputs::CRT::Rect visible_area) {
|
||||
GLfloat outputAspectRatioMultiplier = (static_cast<float>(output_width) / static_cast<float>(output_height)) / (4.0f / 3.0f);
|
||||
|
||||
GLfloat bonusWidth = (outputAspectRatioMultiplier - 1.0f) * visible_area.size.width;
|
||||
visible_area.origin.x -= bonusWidth * 0.5f * visible_area.size.width;
|
||||
|
||||
right_extent_ = (1.0f / outputAspectRatioMultiplier) / visible_area.size.width;
|
||||
|
||||
visible_area.origin.x -= bonusWidth * 0.5f;
|
||||
visible_area.size.width *= outputAspectRatioMultiplier;
|
||||
|
||||
set_uniform("boundsOrigin", (GLfloat)visible_area.origin.x, (GLfloat)visible_area.origin.y);
|
||||
set_uniform("boundsSize", (GLfloat)visible_area.size.width, (GLfloat)visible_area.size.height);
|
||||
}
|
||||
|
||||
float OutputShader::get_right_extent() {
|
||||
return right_extent_;
|
||||
}
|
||||
|
||||
void OutputShader::set_source_texture_unit(GLenum unit) {
|
||||
set_uniform("texID", (GLint)(unit - GL_TEXTURE0));
|
||||
}
|
||||
|
||||
@@ -87,6 +87,14 @@ public:
|
||||
space, 0.5 means use half, etc.
|
||||
*/
|
||||
void set_input_width_scaler(float input_scaler);
|
||||
|
||||
/*!
|
||||
@returns The location, in eye coordinates, of the right edge of the output area.
|
||||
*/
|
||||
float get_right_extent();
|
||||
|
||||
private:
|
||||
float right_extent_ = 0.0f;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ class TextureTarget {
|
||||
/*!
|
||||
Draws this texture to the currently-bound framebuffer, which has the aspect ratio
|
||||
@c aspect_ratio. This texture will fill the height of the frame buffer, and pick
|
||||
an appropriate width based o the aspect ratio.
|
||||
an appropriate width based on the aspect ratio.
|
||||
|
||||
@c colour_threshold sets a threshold test that each colour must satisfy to be
|
||||
output. A threshold of 0.0f means that all colours will pass through. A threshold
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Clock Signal
|
||||
Clock Signal ('CLK') is an emulator for tourists that seeks to be invisible. Users directly launch classic software with no emulator or per-emulated-machine learning curve.
|
||||
|
||||
[Releases](https://github.com/TomHarte/CLK/releases) are hosted on Github.
|
||||
[Releases](https://github.com/TomHarte/CLK/releases) are hosted on GitHub.
|
||||
|
||||
On the Mac it is a native Cocoa application. Under Linux, BSD and other UNIXes and UNIX-alikes it relies upon SDL 2.
|
||||
|
||||
@@ -54,7 +54,7 @@ If your machine has a 4k monitor and a 96Khz audio output? Then you'll get a 4k
|
||||
|||
|
||||
|||
|
||||
|
||||
<img src="READMEImages/ReptonInterlaced.gif" height=600 alt="Repton title screen, interlaced">
|
||||
<img src="READMEImages/ReptonInterlaced.gif" height=400 alt="Repton title screen, interlaced"> <img src="READMEImages/AppleIIPrinceOfPersia.png" height=400 alt="Apple IIe Prince of Persia">
|
||||
|
||||
## Low Latency
|
||||
|
||||
|
||||
BIN
READMEImages/AppleIIPrinceOfPersia.png
Normal file
BIN
READMEImages/AppleIIPrinceOfPersia.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 451 KiB |
@@ -2,5 +2,12 @@ ROM files would ordinarily go here; they are copyright Apple so are not included
|
||||
|
||||
Expected files:
|
||||
|
||||
apple2o.rom — a 12kb image of the original Apple II's ROMs.
|
||||
apple2-character.rom — a 2kb image of the Apple II+'s character ROM.
|
||||
apple2o.rom — an image at least 12kb big, in which the final 12kb is the original Apple II's ROM.
|
||||
apple2.rom — an image at least 12kb big, in which the final 12kb is the Apple II+'s ROM.
|
||||
apple2e.rom — a file at least 15.75kb big, in which the final 12kb is the main portion of the Enhanced IIe ROM, that is visible from $D000, and the 3.75kb before that is the portion that can be paged in from $C100.
|
||||
apple2eu.rom — as per apple2e.rom, but for the Unenhanced Apple II.
|
||||
|
||||
apple2-character.rom — a 2kb image of the Apple IIe's character ROM.
|
||||
apple2eu-character.rom — a 4kb image of the Unenhanced IIe's character ROM.
|
||||
|
||||
Apologies for the wackiness around "at least xkb big", it's to allow for use of files such as those on ftp.apple.asimov.net, which tend to be a bunch of other things, then the system ROM.
|
||||
@@ -117,20 +117,22 @@ bool Drive::get_is_ready() {
|
||||
}
|
||||
|
||||
void Drive::set_motor_on(bool motor_is_on) {
|
||||
motor_is_on_ = motor_is_on;
|
||||
if(motor_is_on_ != motor_is_on) {
|
||||
motor_is_on_ = motor_is_on;
|
||||
|
||||
if(observer_) {
|
||||
observer_->set_drive_motor_status(drive_name_, motor_is_on_);
|
||||
if(announce_motor_led_) {
|
||||
observer_->set_led_status(drive_name_, motor_is_on_);
|
||||
if(observer_) {
|
||||
observer_->set_drive_motor_status(drive_name_, motor_is_on_);
|
||||
if(announce_motor_led_) {
|
||||
observer_->set_led_status(drive_name_, motor_is_on_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!motor_is_on) {
|
||||
ready_index_count_ = 0;
|
||||
if(disk_) disk_->flush_tracks();
|
||||
if(!motor_is_on) {
|
||||
ready_index_count_ = 0;
|
||||
if(disk_) disk_->flush_tracks();
|
||||
}
|
||||
update_clocking_observer();
|
||||
}
|
||||
update_clocking_observer();
|
||||
}
|
||||
|
||||
bool Drive::get_motor_on() {
|
||||
|
||||
Reference in New Issue
Block a user