2020-11-01 00:02:50 +00:00
|
|
|
//
|
|
|
|
// VideoSwitches.hpp
|
|
|
|
// Clock Signal
|
|
|
|
//
|
|
|
|
// Created by Thomas Harte on 31/10/2020.
|
|
|
|
// Copyright © 2020 Thomas Harte. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
#ifndef VideoSwitches_h
|
|
|
|
#define VideoSwitches_h
|
|
|
|
|
|
|
|
#include "../../../ClockReceiver/ClockReceiver.hpp"
|
|
|
|
#include "../../../ClockReceiver/DeferredQueue.hpp"
|
2020-11-07 22:45:03 +00:00
|
|
|
#include "../../ROMMachine.hpp"
|
2020-11-01 00:02:50 +00:00
|
|
|
|
2023-05-10 21:02:18 +00:00
|
|
|
namespace Apple::II {
|
2020-11-01 00:02:50 +00:00
|
|
|
|
2020-11-23 02:29:40 +00:00
|
|
|
// Enumerates all Apple II and IIe display modes.
|
2020-11-01 00:02:50 +00:00
|
|
|
enum class GraphicsMode {
|
|
|
|
Text = 0,
|
|
|
|
DoubleText,
|
|
|
|
HighRes,
|
|
|
|
DoubleHighRes,
|
|
|
|
LowRes,
|
|
|
|
DoubleLowRes,
|
2020-11-24 01:58:32 +00:00
|
|
|
/// Fat low res mode is regular low res mode, but clocked out at 7Mhz rather than 14, leading to improper colours.
|
2020-11-01 00:02:50 +00:00
|
|
|
FatLowRes
|
|
|
|
};
|
|
|
|
constexpr bool is_text_mode(GraphicsMode m) { return m <= GraphicsMode::DoubleText; }
|
|
|
|
constexpr bool is_double_mode(GraphicsMode m) { return int(m) & 1; }
|
|
|
|
|
|
|
|
template <typename TimeUnit> class VideoSwitches {
|
|
|
|
public:
|
|
|
|
/*!
|
2020-11-01 00:39:32 +00:00
|
|
|
Constructs a new instance of VideoSwitches in which changes to relevant switches
|
2020-11-01 00:02:50 +00:00
|
|
|
affect the video mode only after @c delay cycles.
|
2020-11-07 22:45:03 +00:00
|
|
|
|
|
|
|
If @c is_iie is true, these switches will set up the character zones for an IIe-esque
|
|
|
|
set of potential flashing characters and alternate video modes.
|
2020-11-01 00:02:50 +00:00
|
|
|
*/
|
2020-11-07 22:45:03 +00:00
|
|
|
VideoSwitches(bool is_iie, TimeUnit delay, std::function<void(TimeUnit)> &&target) : delay_(delay), deferrer_(std::move(target)) {
|
|
|
|
character_zones_[0].xor_mask = 0;
|
|
|
|
character_zones_[0].address_mask = 0x3f;
|
|
|
|
character_zones_[1].xor_mask = 0;
|
|
|
|
character_zones_[1].address_mask = 0x3f;
|
|
|
|
character_zones_[2].xor_mask = 0;
|
|
|
|
character_zones_[2].address_mask = 0x3f;
|
|
|
|
character_zones_[3].xor_mask = 0;
|
|
|
|
character_zones_[3].address_mask = 0x3f;
|
|
|
|
|
|
|
|
if(is_iie) {
|
|
|
|
character_zones_[0].xor_mask =
|
|
|
|
character_zones_[2].xor_mask =
|
|
|
|
character_zones_[3].xor_mask = 0xff;
|
|
|
|
character_zones_[2].address_mask =
|
|
|
|
character_zones_[3].address_mask = 0xff;
|
|
|
|
}
|
|
|
|
}
|
2020-11-01 00:02:50 +00:00
|
|
|
|
|
|
|
/*!
|
|
|
|
Advances @c cycles.
|
|
|
|
*/
|
|
|
|
void run_for(TimeUnit cycles) {
|
|
|
|
deferrer_.run_for(cycles);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
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 alternative_character_set) {
|
|
|
|
external_.alternative_character_set = alternative_character_set;
|
|
|
|
deferrer_.defer(delay_, [this, alternative_character_set] {
|
|
|
|
internal_.alternative_character_set = alternative_character_set;
|
2020-11-07 22:45:03 +00:00
|
|
|
|
|
|
|
if(alternative_character_set) {
|
|
|
|
character_zones_[1].address_mask = 0xff;
|
|
|
|
character_zones_[1].xor_mask = 0;
|
|
|
|
} else {
|
|
|
|
character_zones_[1].address_mask = 0x3f;
|
|
|
|
character_zones_[1].xor_mask = flash_mask();
|
|
|
|
// The XOR mask is seeded here; it's dynamic, so updated elsewhere.
|
|
|
|
}
|
2020-11-01 00:02:50 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
bool get_alternative_character_set() const {
|
|
|
|
return external_.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 columns_80) {
|
|
|
|
external_.columns_80 = columns_80;
|
|
|
|
deferrer_.defer(delay_, [this, columns_80] {
|
|
|
|
internal_.columns_80 = columns_80;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
bool get_80_columns() const {
|
|
|
|
return external_.columns_80;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
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 store_80) {
|
|
|
|
external_.store_80 = internal_.store_80 = store_80;
|
|
|
|
}
|
|
|
|
bool get_80_store() const {
|
|
|
|
return external_.store_80;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
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 page2) {
|
|
|
|
external_.page2 = internal_.page2 = page2;
|
|
|
|
}
|
|
|
|
bool get_page2() const {
|
|
|
|
return external_.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 text) {
|
|
|
|
external_.text = text;
|
|
|
|
deferrer_.defer(delay_, [this, text] {
|
|
|
|
internal_.text = text;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
bool get_text() const {
|
|
|
|
return external_.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 mixed) {
|
|
|
|
external_.mixed = mixed;
|
|
|
|
deferrer_.defer(delay_, [this, mixed] {
|
|
|
|
internal_.mixed = mixed;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
bool get_mixed() const {
|
|
|
|
return external_.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 high_resolution) {
|
|
|
|
external_.high_resolution = high_resolution;
|
|
|
|
deferrer_.defer(delay_, [this, high_resolution] {
|
|
|
|
internal_.high_resolution = high_resolution;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
bool get_high_resolution() const {
|
|
|
|
return external_.high_resolution;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*!
|
|
|
|
Setter for annunciator 3.
|
|
|
|
|
|
|
|
* On: turn on annunciator 3.
|
|
|
|
* Off: turn off annunciator 3.
|
|
|
|
|
|
|
|
This exists on both the II/II+ and the IIe, but has no effect on
|
|
|
|
video on the older machines. It's intended to be used on the IIe
|
|
|
|
to confirm double-high resolution mode but has side effects in
|
|
|
|
selecting mixed mode output and discarding high-resolution
|
|
|
|
delay bits.
|
|
|
|
*/
|
|
|
|
void set_annunciator_3(bool annunciator_3) {
|
|
|
|
external_.annunciator_3 = annunciator_3;
|
|
|
|
deferrer_.defer(delay_, [this, annunciator_3] {
|
|
|
|
internal_.annunciator_3 = annunciator_3;
|
2020-11-07 22:45:03 +00:00
|
|
|
high_resolution_mask_ = annunciator_3 ? 0x7f : 0xff;
|
2020-11-01 00:02:50 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
bool get_annunciator_3() const {
|
|
|
|
return external_.annunciator_3;
|
|
|
|
}
|
|
|
|
|
2020-11-07 22:45:03 +00:00
|
|
|
/// Set the character ROM for this video output.
|
|
|
|
void set_character_rom(const std::vector<uint8_t> &rom) {
|
|
|
|
character_rom_ = rom;
|
|
|
|
|
|
|
|
// There's some inconsistency in bit ordering amongst the common ROM dumps;
|
|
|
|
// detect that based arbitrarily on the second line of the $ graphic and
|
|
|
|
// ensure consistency.
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-01 00:02:50 +00:00
|
|
|
protected:
|
|
|
|
GraphicsMode graphics_mode(int row) const {
|
|
|
|
if(
|
|
|
|
internal_.text ||
|
|
|
|
(internal_.mixed && row >= 160 && row < 192)
|
|
|
|
) return internal_.columns_80 ? GraphicsMode::DoubleText : GraphicsMode::Text;
|
|
|
|
if(internal_.high_resolution) {
|
|
|
|
return (internal_.annunciator_3 && internal_.columns_80) ? GraphicsMode::DoubleHighRes : GraphicsMode::HighRes;
|
|
|
|
} else {
|
|
|
|
if(internal_.columns_80) return GraphicsMode::DoubleLowRes;
|
|
|
|
if(internal_.annunciator_3) return GraphicsMode::FatLowRes;
|
|
|
|
return GraphicsMode::LowRes;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
int video_page() const {
|
|
|
|
return (internal_.store_80 || !internal_.page2) ? 0 : 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
uint16_t get_row_address(int row) const {
|
|
|
|
const int character_row = row >> 3;
|
|
|
|
const int pixel_row = row & 7;
|
|
|
|
const uint16_t row_address = uint16_t((character_row >> 3) * 40 + ((character_row&7) << 7));
|
|
|
|
|
|
|
|
const GraphicsMode pixel_mode = graphics_mode(row);
|
|
|
|
return ((pixel_mode == GraphicsMode::HighRes) || (pixel_mode == GraphicsMode::DoubleHighRes)) ?
|
|
|
|
uint16_t(((video_page()+1) * 0x2000) + row_address + ((pixel_row&7) << 10)) :
|
|
|
|
uint16_t(((video_page()+1) * 0x400) + row_address);
|
|
|
|
}
|
|
|
|
|
2020-11-07 22:45:03 +00:00
|
|
|
/*!
|
|
|
|
Should be called by subclasses at the end of each line of the display;
|
|
|
|
this gives the base class a peg on which to hang flashing-character updates.
|
|
|
|
*/
|
|
|
|
void did_end_line() {
|
|
|
|
// Update character set flashing; flashing is applied only when the alternative
|
|
|
|
// character set is not selected.
|
|
|
|
flash_ = (flash_ + 1) % (2 * flash_length);
|
|
|
|
character_zones_[1].xor_mask = flash_mask() * !internal_.alternative_character_set;
|
|
|
|
}
|
2020-11-01 00:02:50 +00:00
|
|
|
|
|
|
|
private:
|
|
|
|
// Maintain a DeferredQueue for delayed mode switches.
|
|
|
|
const TimeUnit delay_;
|
|
|
|
DeferredQueuePerformer<TimeUnit> deferrer_;
|
|
|
|
|
|
|
|
struct Switches {
|
|
|
|
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 annunciator_3 = false;
|
|
|
|
} external_, internal_;
|
2020-11-07 22:45:03 +00:00
|
|
|
|
|
|
|
int flash_length = 8406;
|
|
|
|
int flash_ = 0;
|
|
|
|
uint8_t flash_mask() const {
|
|
|
|
return uint8_t((flash_ / flash_length) * 0xff);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
|
|
|
|
|
|
|
// Describes the current text mode mapping from in-memory character index
|
|
|
|
// to output character; subclasses should:
|
|
|
|
//
|
|
|
|
// (i) use the top two-bits of the character code to index character_zones_;
|
|
|
|
// (ii) apply the address_mask to the character code in order to get a character
|
|
|
|
// offset into the character ROM; and
|
|
|
|
// (iii) apply the XOR mask to the output of the character ROM.
|
|
|
|
//
|
|
|
|
// By this means they will properly handle the limited character sets of Apple IIs
|
|
|
|
// prior to the IIe as well as the IIe and onward's alternative character set toggle.
|
|
|
|
struct CharacterMapping {
|
|
|
|
uint8_t address_mask;
|
|
|
|
uint8_t xor_mask;
|
|
|
|
};
|
|
|
|
CharacterMapping character_zones_[4];
|
|
|
|
|
|
|
|
// A mask that should be applied to high-resolution graphics bytes before output;
|
|
|
|
// it acts to retain or remove the top bit, affecting whether the half-pixel delay
|
|
|
|
// bit is effective. On a IIe it's toggleable, on early Apple IIs it doesn't exist.
|
|
|
|
uint8_t high_resolution_mask_ = 0xff;
|
|
|
|
|
|
|
|
// 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_;
|
2020-11-01 00:02:50 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
#endif /* VideoSwitches_h */
|