1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-10-25 09:27:01 +00:00

Compare commits

...

85 Commits

Author SHA1 Message Date
Thomas Harte
c2d9e1ec81 Merge pull request #515 from TomHarte/POPImage
Adds an Apple II screenshot to the readme.
2018-08-05 17:58:53 -04:00
Thomas Harte
673b915ee8 Reduces post-table image size a little. 2018-08-05 17:57:37 -04:00
Thomas Harte
032a62dfff Adds An Apple II screenshot to the mix. 2018-08-05 17:56:10 -04:00
Thomas Harte
f2d78182a3 Merge pull request #514 from TomHarte/VideoFixes
Fixes various IIe video deficiencies.
2018-08-05 17:49:13 -04:00
Thomas Harte
de68e70246 Fixes various IIe video deficiencies.
Specifically:
* the double-high resolution switches should be read/write; and
* the other IIe-specific switches should cause a video update for real-time effect.
2018-08-05 17:47:23 -04:00
Thomas Harte
e07447eb9a Merge pull request #513 from TomHarte/JoystickRange
Significant improves Apple II joystick compatibility
2018-08-05 17:37:38 -04:00
Thomas Harte
5cdeb58571 Makes digital to analogue conversion more extreme. 2018-08-05 17:36:20 -04:00
Thomas Harte
ce14cc8677 Flips meaning of analogue input bits, correcting most joystick titles.
Mysteriously, some functioned correctly before this. But they continue to do so.
2018-08-05 17:36:01 -04:00
Thomas Harte
f7ce86fef8 Merge pull request #512 from TomHarte/80Text
Extends correct text handling to 80-column mode.
2018-08-04 22:28:59 -04:00
Thomas Harte
55f2fccf5e Extends correct text handling to 80-column mode. 2018-08-04 22:25:29 -04:00
Thomas Harte
101fb5d7bf Merge pull request #511 from TomHarte/ColecoSizeCheck
Relaxes ColecoVision cartridge image size check
2018-08-04 21:47:30 -04:00
Thomas Harte
3c51e335c3 Makes extra sure not to try to read from an empty characters list. 2018-08-04 21:40:26 -04:00
Thomas Harte
33ea90678c Relaxes ColecoVision cartridge size test. 2018-08-04 21:40:02 -04:00
Thomas Harte
11ae2c64ba Merge pull request #502 from TomHarte/IIe
Extends Apple II emulation to include the IIe
2018-08-04 21:02:45 -04:00
Thomas Harte
26624d7652 Fixes vertical blank signal; it should be the other way around. 2018-08-04 20:57:02 -04:00
Thomas Harte
85fb4773b0 Tweaks Apple key mapping and implements reset_all_keys. 2018-08-04 20:31:37 -04:00
Thomas Harte
099d66804e Makes colour burst phase explicit. 2018-08-04 19:29:34 -04:00
Thomas Harte
086596c28e Adds reading of vertical blank and implements the full IIe keyboard logic.
i.e. there are now two Apple keys, and shift isn't assumed.
2018-08-04 19:17:04 -04:00
Thomas Harte
3aeb4213fe Implements the C010 read value. 2018-08-04 17:57:02 -04:00
Thomas Harte
558b96bc05 Corrects IIe text display. 2018-08-04 16:52:29 -04:00
Thomas Harte
e97cc40a2c Corrects typo in Cx-page ROM paging. 2018-08-04 12:44:58 -04:00
Thomas Harte
94503ed771 Disables the macOS Apple II options panel, since it now has no options. 2018-08-04 12:37:55 -04:00
Thomas Harte
c4f86cc324 The Disk II now being its proper speed, withdraws the quickload option. 2018-08-03 21:20:21 -04:00
Thomas Harte
70c4d6b9b3 Adds a one second delay between controller and drive motor off. 2018-08-03 21:13:18 -04:00
Thomas Harte
78c7137427 Avoids observer communication if motor status hasn't changed. 2018-08-03 21:11:22 -04:00
Thomas Harte
74a2f717b3 Turns down the composite signal amplitude a little, to help colour distinctness. 2018-08-01 18:52:42 -04:00
Thomas Harte
98bb5bd9f1 Ensures flux bits are observable for two cycles rather than one; it should be 1us. 2018-07-31 23:01:11 -04:00
Thomas Harte
c91eaaf8da Takes a stab at double low-res graphics. 2018-07-31 21:45:09 -04:00
Thomas Harte
a36f37d240 Introduces a 1/14th delay in output of double high res. 2018-07-31 21:29:51 -04:00
Thomas Harte
c773d3501a Implements the INTC8ROM switch.
Finally causing the Zellyn tests to pass! Is this nightmare behind me?
2018-07-31 19:00:46 -04:00
Thomas Harte
5810f9b3f9 Fixes high resolution address range and switching logic. 2018-07-30 23:23:18 -04:00
Thomas Harte
3f56683342 Fixes order of deserialisation between auxiliary and base RAM. 2018-07-30 23:08:45 -04:00
Thomas Harte
16ccbdefd6 Of course, | has higher precedence than ?. Classic! 2018-07-30 23:08:22 -04:00
Thomas Harte
a533d09fe7 Sets the IIe as the default model. 2018-07-30 23:07:34 -04:00
Thomas Harte
e9aaa5bbdf Factors out the page-mapping function.
For one less potential source of failure.
2018-07-30 22:23:48 -04:00
Thomas Harte
ecb26e3281 Corrections: slot_C3_rom_ works the other way around; 80STORE doesn't affect most of RAM but does always affect the text screen.
Also factored out `set_zero_page_paging` for consistency.
2018-07-30 19:54:25 -04:00
Thomas Harte
5aa0b17720 Improves IIe paging further. 2018-07-29 23:02:27 -04:00
Thomas Harte
632b37ecec Attempts an implementation of auxiliary memory. 2018-07-29 10:41:12 -04:00
Thomas Harte
c905de2e40 Restores IIe ROM-over-card paging. 2018-07-28 13:31:25 -04:00
Thomas Harte
bc2afe69e1 Accepting that memory mapping on a IIe is more complicated than I anticiapted, introduces mapping for all pages.
Also picks a name for the Unenhanced Apple IIe ROM.
2018-07-28 13:02:49 -04:00
Thomas Harte
894998b163 Merge branch 'master' into IIe 2018-07-28 10:54:04 -04:00
Thomas Harte
51192d8397 Merge pull request #508 from TomHarte/Whitespace
Eliminates various blank lines.
2018-07-28 10:53:17 -04:00
Thomas Harte
3c33ccd730 Eliminates various blank lines. 2018-07-28 10:52:34 -04:00
Thomas Harte
3e35109d63 Merge pull request #507 from TomHarte/BetterBMPDestination
Use `xdg-user-dir PICTURES` instead of $HOME for screenshots
2018-07-28 10:48:28 -04:00
Thomas Harte
99c770eab4 Ensure that the output of xdg-user-dir is properly filtered. 2018-07-28 10:45:50 -04:00
Thomas Harte
34aa78b7ce Attempts to use xdg-user-dir PICTURES in preference to $HOME for pictures. 2018-07-28 09:14:18 -04:00
Thomas Harte
8cca9c2055 Merge branch 'master' into IIe 2018-07-27 23:52:39 -04:00
Thomas Harte
85ce21c79f Merge pull request #505 from TomHarte/MacScreenshots
Attempts to introduce screenshot capture for macOS.
2018-07-27 23:43:13 -04:00
Thomas Harte
d19d949b9c Removes unnecessary import. 2018-07-27 23:41:55 -04:00
Thomas Harte
1cb3713b84 Attempts to introduce screenshot capture for macOS. 2018-07-27 23:37:24 -04:00
Thomas Harte
689850d698 Merge pull request #504 from TomHarte/SDLBMPByteOrder
Ensures SDL is properly informed of buffer byte order.
2018-07-27 18:53:16 -04:00
Thomas Harte
c572a52049 Ensures SDL is properly informed of buffer byte order. 2018-07-27 18:51:38 -04:00
Thomas Harte
41765e00c4 Merge branch 'master' into IIe 2018-07-26 21:24:46 -04:00
Thomas Harte
080aa0acc5 Merge pull request #503 from TomHarte/SDLScreenshots
Adds screenshot saving upon ctrl+shift+d.
2018-07-26 20:58:35 -04:00
Thomas Harte
5e7c46a72a Adds screenshot saving upon ctrl+shift+d. 2018-07-26 20:53:12 -04:00
Thomas Harte
5f2b9b2d5a Implements the alternative zero page soft switch. 2018-07-25 22:10:21 -04:00
Thomas Harte
5c4506a9db Talks the IIe into proceeding to a beep and an improperly-formed logo. 2018-07-25 21:43:12 -04:00
Thomas Harte
55a6431fb3 Puts in enough logic to be able to launch a non-functional IIe. 2018-07-25 18:58:34 -04:00
Thomas Harte
ede2696a77 Edges further towards implementing the IIe video subsystem.
All video-specific switches are in place, and mostly honoured, and a IIe machine configuration is advertised at least.
2018-07-24 22:15:42 -04:00
Thomas Harte
59b9e39022 Starts the process of supporting the Apple IIe graphics modes.
Albeit that I'm not yet even up on the proper soft switches.
2018-07-23 22:14:41 -04:00
Thomas Harte
6b2970f2f2 Ensures no-hat input doesn't override analogue axes. 2018-07-22 17:29:37 -04:00
Thomas Harte
6a73fe7d65 Merge pull request #500 from TomHarte/MacJoysticks
Implements initial joystick support for the Mac
2018-07-22 16:56:40 -04:00
Thomas Harte
1362906f94 Wires joystick support all the way through to machines.
Ensures there's only one joystick manager, which is shared by all machines, with input going only to the key window.
2018-07-22 16:55:47 -04:00
Thomas Harte
8f4042c4bb Permits joysticks to be queried for number of fire buttons. 2018-07-22 16:52:58 -04:00
Thomas Harte
c05b6397b0 Attempts a full implementation of the joystick manager.
So it currently vends a list of existing joysticks plus their states. More work will be required for a UI — e.g. there is no way to identify one joystick from another — but this'll do for now.
2018-07-22 15:23:26 -04:00
Thomas Harte
8d18808efe Walks a few steps further along device inspection. 2018-07-20 23:33:04 -04:00
Thomas Harte
09950d9414 Gamely starts to create a HID input manager for joysticks/pads/etc. 2018-07-19 22:43:01 -04:00
Thomas Harte
badbbdf155 Merge pull request #498 from TomHarte/DisplayBorder
Resolves border issues in fullscreen mode
2018-07-16 22:01:08 -04:00
Thomas Harte
2832792fed Corrects improper use of doubles. 2018-07-16 21:55:19 -04:00
Thomas Harte
efa45b9504 Adds a right gutter to clip persistence errors.
Also uncovers and corrects a long-standing centring error.
2018-07-16 21:52:31 -04:00
Thomas Harte
523749edf8 Merge branch 'master' into DisplayBorder 2018-07-16 20:00:52 -04:00
Thomas Harte
5a0499e8a7 Merge pull request #499 from TomHarte/EditorConfig
Adds a .editorconfig to aid Github display.
2018-07-16 20:00:27 -04:00
Thomas Harte
258c8b5900 Adds a .editorconfig to aid Github display. 2018-07-16 19:59:03 -04:00
Thomas Harte
24b861f056 Eliminates make_unique as this is presently a C++11 project. 2018-07-15 22:52:36 -04:00
Thomas Harte
29f7f4d432 Adds missing #include. 2018-07-15 22:47:50 -04:00
Thomas Harte
21080a1149 Merge branch 'master' into DisplayBorder 2018-07-15 22:31:33 -04:00
Thomas Harte
1d068fd09b Merge pull request #497 from TomHarte/RobocopSprites
Ensures only the first 8px of sprites is output in 8x8 mode.
2018-07-15 22:30:42 -04:00
Thomas Harte
92065813ef Ensures only the first 8px of sprites is output in 8x8 mode.
Also adds a little extra documentation.
2018-07-15 22:21:29 -04:00
Thomas Harte
3e9ef6b8cb Adds indicator lights for the SDL port.
To complete #426
2018-07-15 20:19:06 -04:00
Thomas Harte
c9451a5382 Introduces an object for drawing OpenGL rectangles. 2018-07-14 17:42:23 -04:00
Thomas Harte
2be3b027db Merge branch 'master' into DisplayBorder 2018-07-14 13:13:29 -04:00
Thomas Harte
e339d169c5 Ensures the joystick doesn't obstruct tape input. 2018-07-12 22:10:05 -04:00
Thomas Harte
87001f86ee Merge pull request #495 from TomHarte/MSXJoysticks
Adds joystick support for the MSX.
2018-07-12 21:44:26 -04:00
Thomas Harte
58484e8f37 Adds joystick support for the MSX. 2018-07-12 21:42:47 -04:00
Thomas Harte
00cb4d26b3 Corrects typo. 2018-07-11 19:52:55 -04:00
43 changed files with 1790 additions and 331 deletions

6
.editorconfig Normal file
View File

@@ -0,0 +1,6 @@
[*]
charset = utf-8
indent_style = tab
indent_size = 4
trim_trailing_whitespace = true

View File

@@ -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) {}
};
}

View File

@@ -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;
};

View File

@@ -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;

View File

@@ -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_++;
}

View File

@@ -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;

View File

@@ -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;
};
}

View File

@@ -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) {}

View File

@@ -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;
}
}

View File

@@ -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() {}

View File

@@ -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();

View File

@@ -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);
}
}
}

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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());

View File

@@ -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()));

View File

@@ -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 */,

View File

@@ -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>

View File

@@ -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>

View File

@@ -12,6 +12,8 @@
#import "CSOpenGLView.h"
#import "CSAudioQueue.h"
#import "CSBestEffortUpdater.h"
#import "CSJoystickManager.h"
#include "KeyCodes.h"

View File

@@ -9,4 +9,5 @@
import Cocoa
class DocumentController: NSDocumentController {
let joystickManager = CSJoystickManager()
}

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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;

View File

@@ -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);

View File

@@ -12,7 +12,8 @@
typedef NS_ENUM(NSInteger, CSMachineAppleIIModel) {
CSMachineAppleIIModelAppleII,
CSMachineAppleIIModelAppleIIPlus
CSMachineAppleIIModelAppleIIPlus,
CSMachineAppleIIModelAppleIIe
};
typedef NS_ENUM(NSInteger, CSMachineAppleIIDiskController) {

View File

@@ -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";

View File

@@ -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"/>

View File

@@ -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
}

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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);

View 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);
}

View 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 */

View File

@@ -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,

View File

@@ -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));
}

View File

@@ -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;
};
}

View File

@@ -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

View File

@@ -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
|![Amstrad text, with a classic 1:1 pixel emulation](READMEImages/NaiveCPC.png)|![Amstrad text, with correct aspect ratio and subject to a lowpass filter](READMEImages/FilteredCPC.png)|
|![The Amstrad CPC version of Stormlord, with a classic 1:1 pixel emulation](READMEImages/NaiveCPCStormlord.png)|![The Amstrad CPC version of Stormlord, with correct aspect ratio and subject to a lowpass filter](READMEImages/CPCStormlord.png)|
<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

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

View File

@@ -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.

View File

@@ -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() {