2020-11-01 00:39:32 +00:00
|
|
|
//
|
|
|
|
// Video.cpp
|
|
|
|
// Clock Signal
|
|
|
|
//
|
|
|
|
// Created by Thomas Harte on 31/10/2020.
|
|
|
|
// Copyright © 2020 Thomas Harte. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
#include "Video.hpp"
|
|
|
|
|
|
|
|
using namespace Apple::IIgs::Video;
|
|
|
|
|
2020-11-05 22:56:20 +00:00
|
|
|
namespace {
|
|
|
|
|
2020-11-08 01:42:34 +00:00
|
|
|
constexpr int CyclesPerTick = 7; // One 'tick' being the non-stretched length of a cycle on the old Apple II 1Mhz clock.
|
|
|
|
constexpr int CyclesPerLine = 456; // Each of the Mega II's cycles lasts 7 cycles, making 455/line except for the
|
|
|
|
// final on on a line which lasts an additional 1 (i.e. is 1/7th longer).
|
2020-11-05 22:56:20 +00:00
|
|
|
constexpr int Lines = 263;
|
|
|
|
constexpr int FinalPixelLine = 192;
|
|
|
|
|
2020-11-08 01:42:34 +00:00
|
|
|
constexpr auto FinalColumn = CyclesPerLine / CyclesPerTick;
|
|
|
|
|
2020-11-08 03:03:05 +00:00
|
|
|
// Converts from Apple's RGB ordering to this emulator's.
|
2020-11-12 01:41:30 +00:00
|
|
|
#if TARGET_RT_BIG_ENDIAN
|
2020-11-15 23:36:24 +00:00
|
|
|
#define PaletteConvulve(x) uint16_t(x)
|
2020-11-12 01:41:30 +00:00
|
|
|
#else
|
2020-11-15 23:36:24 +00:00
|
|
|
#define PaletteConvulve(x) uint16_t(((x&0xf00) >> 8) | ((x&0x0ff) << 8))
|
2020-11-12 01:41:30 +00:00
|
|
|
#endif
|
2020-11-08 03:03:05 +00:00
|
|
|
|
|
|
|
// The 12-bit values used by the Apple IIgs to approximate Apple II colours,
|
|
|
|
// as implied by tech note #63's use of them as border colours.
|
|
|
|
// http://www.1000bit.it/support/manuali/apple/technotes/iigs/tn.iigs.063.html
|
|
|
|
constexpr uint16_t appleii_palette[16] = {
|
|
|
|
PaletteConvulve(0x0000), // Black.
|
|
|
|
PaletteConvulve(0x0d03), // Deep Red.
|
|
|
|
PaletteConvulve(0x0009), // Dark Blue.
|
|
|
|
PaletteConvulve(0x0d2d), // Purple.
|
|
|
|
PaletteConvulve(0x0072), // Dark Green.
|
|
|
|
PaletteConvulve(0x0555), // Dark Gray.
|
|
|
|
PaletteConvulve(0x022f), // Medium Blue.
|
|
|
|
PaletteConvulve(0x06af), // Light Blue.
|
|
|
|
PaletteConvulve(0x0850), // Brown.
|
|
|
|
PaletteConvulve(0x0f60), // Orange.
|
|
|
|
PaletteConvulve(0x0aaa), // Light Grey.
|
|
|
|
PaletteConvulve(0x0f98), // Pink.
|
|
|
|
PaletteConvulve(0x01d0), // Light Green.
|
|
|
|
PaletteConvulve(0x0ff0), // Yellow.
|
|
|
|
PaletteConvulve(0x04f9), // Aquamarine.
|
|
|
|
PaletteConvulve(0x0fff), // White.
|
|
|
|
};
|
|
|
|
|
2020-11-05 22:56:20 +00:00
|
|
|
}
|
|
|
|
|
2020-11-01 00:39:32 +00:00
|
|
|
VideoBase::VideoBase() :
|
2020-11-08 01:42:34 +00:00
|
|
|
VideoSwitches<Cycles>(true, Cycles(2), [this] (Cycles cycles) { advance(cycles); }),
|
2020-11-08 02:10:05 +00:00
|
|
|
crt_(CyclesPerLine - 1, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Red4Green4Blue4) {
|
2020-11-08 03:03:05 +00:00
|
|
|
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
|
2020-11-16 00:14:43 +00:00
|
|
|
crt_.set_visible_area(Outputs::Display::Rect(0.097f, 0.1f, 0.85f, 0.85f));
|
2020-11-21 23:07:51 +00:00
|
|
|
|
|
|
|
// Establish the shift lookup table for NTSC -> RGB output.
|
2020-11-22 04:39:58 +00:00
|
|
|
for(size_t c = 0; c < sizeof(ntsc_shift_lookup_) / sizeof(*ntsc_shift_lookup_); c++) {
|
2020-11-21 23:07:51 +00:00
|
|
|
const auto top_nibble = c >> 4;
|
|
|
|
|
|
|
|
// Otherwise, check for descending disagreements.
|
2020-11-22 04:39:58 +00:00
|
|
|
ntsc_shift_lookup_[c] = 0;
|
|
|
|
decltype(c) mask = 0x01;
|
|
|
|
while(ntsc_shift_lookup_[c] < 4) {
|
2020-11-21 23:07:51 +00:00
|
|
|
if((top_nibble & mask) != (c & mask)) break;
|
2020-11-22 04:39:58 +00:00
|
|
|
mask <<= 1;
|
|
|
|
++ntsc_shift_lookup_[c];
|
2020-11-21 23:07:51 +00:00
|
|
|
}
|
|
|
|
|
2020-11-22 04:39:58 +00:00
|
|
|
// If no incompatibilities were found then the low four bits are identical
|
|
|
|
// to the top four; can just leave the lookup at a shift of 0 as 0 == 4.
|
2020-11-21 23:07:51 +00:00
|
|
|
}
|
2020-11-08 01:42:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void VideoBase::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
|
|
|
crt_.set_scan_target(scan_target);
|
|
|
|
}
|
|
|
|
|
|
|
|
Outputs::Display::ScanStatus VideoBase::get_scaled_scan_status() const {
|
|
|
|
return crt_.get_scaled_scan_status();
|
|
|
|
}
|
|
|
|
|
|
|
|
void VideoBase::set_display_type(Outputs::Display::DisplayType display_type) {
|
|
|
|
crt_.set_display_type(display_type);
|
|
|
|
}
|
|
|
|
|
|
|
|
Outputs::Display::DisplayType VideoBase::get_display_type() const {
|
|
|
|
return crt_.get_display_type();
|
2020-11-01 00:39:32 +00:00
|
|
|
}
|
|
|
|
|
2020-11-05 23:17:21 +00:00
|
|
|
void VideoBase::set_internal_ram(const uint8_t *ram) {
|
|
|
|
ram_ = ram;
|
|
|
|
}
|
|
|
|
|
2020-11-07 22:45:03 +00:00
|
|
|
void VideoBase::advance(Cycles cycles) {
|
2020-11-08 01:42:34 +00:00
|
|
|
const int column_start = (cycles_into_frame_ % CyclesPerLine) / CyclesPerTick;
|
|
|
|
const int row_start = cycles_into_frame_ / CyclesPerLine;
|
|
|
|
|
2020-11-05 22:56:20 +00:00
|
|
|
cycles_into_frame_ = (cycles_into_frame_ + cycles.as<int>()) % (CyclesPerLine * Lines);
|
2020-11-05 23:17:21 +00:00
|
|
|
|
2020-11-08 01:42:34 +00:00
|
|
|
const int column_end = (cycles_into_frame_ % CyclesPerLine) / CyclesPerTick;
|
|
|
|
const int row_end = cycles_into_frame_ / CyclesPerLine;
|
|
|
|
|
|
|
|
if(row_end == row_start) {
|
2020-11-08 02:28:08 +00:00
|
|
|
if(column_end != column_start) {
|
|
|
|
output_row(row_start, column_start, column_end);
|
|
|
|
}
|
2020-11-08 01:42:34 +00:00
|
|
|
} else {
|
2020-11-08 02:28:08 +00:00
|
|
|
if(column_start != FinalColumn) {
|
|
|
|
output_row(row_start, column_start, FinalColumn);
|
|
|
|
}
|
2020-11-08 01:42:34 +00:00
|
|
|
for(int row = row_start+1; row < row_end; row++) {
|
|
|
|
output_row(row, 0, FinalColumn);
|
|
|
|
}
|
|
|
|
if(column_end) {
|
|
|
|
output_row(row_end, 0, column_end);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-16 19:42:50 +00:00
|
|
|
Cycles VideoBase::get_next_sequence_point() const {
|
|
|
|
const int cycles_into_row = cycles_into_frame_ % CyclesPerLine;
|
|
|
|
const int row = cycles_into_frame_ / CyclesPerLine;
|
|
|
|
|
|
|
|
constexpr int sequence_point_offset = (5 + 8) * CyclesPerTick;
|
|
|
|
|
|
|
|
// Handle every case that doesn't involve wrapping to the next row 0.
|
|
|
|
if(row <= 200) {
|
|
|
|
if(cycles_into_row < sequence_point_offset) return Cycles(sequence_point_offset - cycles_into_row);
|
|
|
|
if(row < 200) return Cycles(CyclesPerLine + sequence_point_offset - cycles_into_row);
|
|
|
|
}
|
|
|
|
|
2020-11-17 00:00:11 +00:00
|
|
|
// Calculate distance to the relevant point in row 0.
|
2020-11-16 19:42:50 +00:00
|
|
|
return Cycles(CyclesPerLine + sequence_point_offset - cycles_into_row + (Lines - row - 1)*CyclesPerLine);
|
|
|
|
}
|
|
|
|
|
2020-11-08 01:42:34 +00:00
|
|
|
void VideoBase::output_row(int row, int start, int end) {
|
|
|
|
// Reasoned guesswork ahoy!
|
|
|
|
//
|
|
|
|
// The IIgs VGC can fetch four bytes per column — I'm unclear physically how, but that's definitely true
|
|
|
|
// since the IIgs modes packs 160 bytes work of graphics into the Apple II's usual 40-cycle fetch area;
|
|
|
|
// it's possible that if I understood the meaning of the linear video bit in the new video flag I'd know more.
|
|
|
|
//
|
|
|
|
// Super Hi-Res also fetches 16*2 = 32 bytes of palette and a control byte sometime before each row.
|
|
|
|
// So it needs five windows for that.
|
|
|
|
//
|
|
|
|
// Guessing four cycles of sync, I've chosen to arrange one output row for this emulator as:
|
|
|
|
//
|
2020-11-08 22:01:23 +00:00
|
|
|
// 5 cycles of back porch; [TODO: include a colour burst]
|
2020-11-08 01:42:34 +00:00
|
|
|
// 8 windows left border, the final five of which fetch palette and control if in IIgs mode;
|
|
|
|
// 40 windows of pixel output;
|
|
|
|
// 8 cycles of right border;
|
|
|
|
// 4 cycles of sync (including the extra 1/7th window, as it has to go _somewhere_).
|
|
|
|
//
|
|
|
|
// Otherwise, the first 200 rows may be pixels and the 192 in the middle of those are the II set.
|
|
|
|
constexpr int first_sync_line = 220; // A complete guess. Information needed.
|
|
|
|
|
|
|
|
constexpr int blank_ticks = 5;
|
|
|
|
constexpr int left_border_ticks = 8;
|
|
|
|
constexpr int pixel_ticks = 40;
|
|
|
|
constexpr int right_border_ticks = 8;
|
|
|
|
|
|
|
|
constexpr int start_of_left_border = blank_ticks;
|
|
|
|
constexpr int start_of_pixels = start_of_left_border + left_border_ticks;
|
|
|
|
constexpr int start_of_right_border = start_of_pixels + pixel_ticks;
|
|
|
|
constexpr int start_of_sync = start_of_right_border + right_border_ticks;
|
|
|
|
constexpr int sync_period = CyclesPerLine - start_of_sync*CyclesPerTick;
|
|
|
|
|
|
|
|
// Deal with vertical sync.
|
|
|
|
if(row >= first_sync_line && row < first_sync_line + 3) {
|
|
|
|
// Simplification: just output the whole line at line's end.
|
|
|
|
if(end == FinalColumn) {
|
|
|
|
crt_.output_sync(CyclesPerLine - sync_period);
|
|
|
|
crt_.output_blank(sync_period);
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-11-08 03:23:48 +00:00
|
|
|
// Pixel or pure border => blank as usual.
|
2020-11-08 01:42:34 +00:00
|
|
|
|
2020-11-08 03:23:48 +00:00
|
|
|
// Output blank only at the end of its window.
|
|
|
|
if(start < blank_ticks && end >= blank_ticks) {
|
|
|
|
crt_.output_blank(blank_ticks * CyclesPerTick);
|
|
|
|
start = blank_ticks;
|
|
|
|
if(start == end) return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Possibly output border, pixels, border, if this is a pixel line.
|
2020-11-08 04:14:50 +00:00
|
|
|
if(row < 192 + ((new_video_&0x80) >> 4)) { // i.e. 192 lines for classic Apple II video, 200 for IIgs video.
|
2020-11-08 01:42:34 +00:00
|
|
|
|
|
|
|
// Output left border as far as currently known.
|
|
|
|
if(start >= start_of_left_border && start < start_of_pixels) {
|
2020-11-08 02:10:05 +00:00
|
|
|
const int end_of_period = std::min(start_of_pixels, end);
|
2020-11-08 01:42:34 +00:00
|
|
|
|
2020-11-17 02:55:41 +00:00
|
|
|
if(border_colour_) {
|
|
|
|
uint16_t *const pixel = reinterpret_cast<uint16_t *>(crt_.begin_data(2, 2));
|
|
|
|
if(pixel) *pixel = border_colour_;
|
|
|
|
crt_.output_data((end_of_period - start) * CyclesPerTick, 1);
|
|
|
|
} else {
|
|
|
|
crt_.output_blank((end_of_period - start) * CyclesPerTick);
|
|
|
|
}
|
2020-11-08 02:10:05 +00:00
|
|
|
|
|
|
|
start = end_of_period;
|
|
|
|
if(start == end) return;
|
2020-11-08 01:42:34 +00:00
|
|
|
}
|
|
|
|
|
2020-11-22 04:39:58 +00:00
|
|
|
assert(end > start);
|
|
|
|
|
2020-11-08 22:01:23 +00:00
|
|
|
// Fetch and output such pixels as it is time for.
|
2020-11-08 01:42:34 +00:00
|
|
|
if(start >= start_of_pixels && start < start_of_right_border) {
|
2020-11-08 02:10:05 +00:00
|
|
|
const int end_of_period = std::min(start_of_right_border, end);
|
2020-11-08 01:42:34 +00:00
|
|
|
|
2020-11-08 22:01:23 +00:00
|
|
|
if(start == start_of_pixels) {
|
2020-11-22 04:39:58 +00:00
|
|
|
// printf("Begin\n");
|
2020-11-08 22:01:23 +00:00
|
|
|
// 640 is the absolute most number of pixels that might be generated
|
|
|
|
next_pixel_ = pixels_ = reinterpret_cast<uint16_t *>(crt_.begin_data(640, 2));
|
2020-11-15 23:36:24 +00:00
|
|
|
|
|
|
|
// YUCKY HACK. I do not know when the IIgs fetches its super high-res palette
|
|
|
|
// and control byte. Since I do not know, any guess is equally likely negatively
|
|
|
|
// to affect software. Therefore this hack is as good as any other guess:
|
|
|
|
// assume RAM has magical burst bandwidth, and fetch the whole set instantly.
|
|
|
|
// I could spread this stuff out to allow for real bandwidth, but it'd likely be
|
|
|
|
// no more accurate, while having less of an obvious I-HACKED-THIS red flag attached.
|
|
|
|
line_control_ = ram_[0x19d00 + row];
|
|
|
|
const int palette_base = (line_control_ & 15) * 16 + 0x19e00;
|
|
|
|
for(int c = 0; c < 16; c++) {
|
|
|
|
const int entry = ram_[palette_base + (c << 1)] | (ram_[palette_base + (c << 1) + 1] << 8);
|
|
|
|
palette_[c] = PaletteConvulve(entry);
|
|
|
|
}
|
|
|
|
|
2020-11-16 19:42:50 +00:00
|
|
|
// Post an interrupt if requested.
|
2020-11-17 00:00:11 +00:00
|
|
|
if(line_control_ & 0x40) {
|
|
|
|
set_interrupts(0x20);
|
|
|
|
}
|
2020-11-08 22:01:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if(next_pixel_) {
|
2020-11-15 22:16:52 +00:00
|
|
|
const int window_start = start - start_of_pixels;
|
|
|
|
const int window_end = end_of_period - start_of_pixels;
|
|
|
|
|
|
|
|
if(new_video_ & 0x80) {
|
|
|
|
next_pixel_ = output_super_high_res(next_pixel_, window_start, window_end, row);
|
|
|
|
} else {
|
2020-11-19 00:47:22 +00:00
|
|
|
switch(graphics_mode(row)) {
|
|
|
|
case Apple::II::GraphicsMode::Text:
|
|
|
|
next_pixel_ = output_text(next_pixel_, window_start, window_end, row);
|
|
|
|
break;
|
|
|
|
case Apple::II::GraphicsMode::DoubleText:
|
|
|
|
next_pixel_ = output_double_text(next_pixel_, window_start, window_end, row);
|
|
|
|
break;
|
2020-11-21 23:07:51 +00:00
|
|
|
case Apple::II::GraphicsMode::LowRes:
|
|
|
|
next_pixel_ = output_low_resolution(next_pixel_, window_start, window_end, row);
|
|
|
|
break;
|
|
|
|
case Apple::II::GraphicsMode::HighRes:
|
|
|
|
next_pixel_ = output_high_resolution(next_pixel_, window_start, window_end, row);
|
|
|
|
break;
|
2020-11-19 00:47:22 +00:00
|
|
|
|
|
|
|
default:
|
|
|
|
assert(false);
|
|
|
|
}
|
2020-11-15 22:16:52 +00:00
|
|
|
|
|
|
|
// TODO: support modes other than 40-column text.
|
2020-11-19 00:47:22 +00:00
|
|
|
// if(graphics_mode(row) != Apple::II::GraphicsMode::Text) printf("Outputting incorrect graphics mode!\n");
|
2020-11-08 22:01:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(end_of_period == start_of_right_border) {
|
2020-11-22 04:39:58 +00:00
|
|
|
// printf("End\n");
|
2020-11-08 22:01:23 +00:00
|
|
|
crt_.output_data((start_of_right_border - start_of_pixels) * CyclesPerTick, next_pixel_ ? size_t(next_pixel_ - pixels_) : 1);
|
|
|
|
next_pixel_ = pixels_ = nullptr;
|
|
|
|
}
|
2020-11-08 02:10:05 +00:00
|
|
|
|
|
|
|
start = end_of_period;
|
|
|
|
if(start == end) return;
|
2020-11-08 01:42:34 +00:00
|
|
|
}
|
|
|
|
|
2020-11-22 04:39:58 +00:00
|
|
|
assert(end > start);
|
|
|
|
|
2020-11-08 22:01:23 +00:00
|
|
|
// Output right border as far as currently known.
|
2020-11-08 01:42:34 +00:00
|
|
|
if(start >= start_of_right_border && start < start_of_sync) {
|
2020-11-08 02:10:05 +00:00
|
|
|
const int end_of_period = std::min(start_of_sync, end);
|
2020-11-08 01:42:34 +00:00
|
|
|
|
2020-11-17 02:55:41 +00:00
|
|
|
if(border_colour_) {
|
|
|
|
uint16_t *const pixel = reinterpret_cast<uint16_t *>(crt_.begin_data(2, 2));
|
|
|
|
if(pixel) *pixel = border_colour_;
|
|
|
|
crt_.output_data((end_of_period - start) * CyclesPerTick, 1);
|
|
|
|
} else {
|
|
|
|
crt_.output_blank((end_of_period - start) * CyclesPerTick);
|
|
|
|
}
|
2020-11-08 02:10:05 +00:00
|
|
|
|
2020-11-08 02:28:08 +00:00
|
|
|
// There's no point updating start here; just fall
|
|
|
|
// through to the end == FinalColumn test.
|
2020-11-08 01:42:34 +00:00
|
|
|
}
|
2020-11-08 03:23:48 +00:00
|
|
|
} else {
|
|
|
|
// This line is all border, all the time.
|
|
|
|
if(start >= start_of_left_border && start < start_of_sync) {
|
|
|
|
const int end_of_period = std::min(start_of_sync, end);
|
2020-11-08 01:42:34 +00:00
|
|
|
|
2020-11-17 02:55:41 +00:00
|
|
|
if(border_colour_) {
|
|
|
|
uint16_t *const pixel = reinterpret_cast<uint16_t *>(crt_.begin_data(2, 2));
|
|
|
|
if(pixel) *pixel = border_colour_;
|
|
|
|
crt_.output_data((end_of_period - start) * CyclesPerTick, 1);
|
|
|
|
} else {
|
|
|
|
crt_.output_blank((end_of_period - start) * CyclesPerTick);
|
|
|
|
}
|
2020-11-08 02:10:05 +00:00
|
|
|
|
2020-11-08 03:23:48 +00:00
|
|
|
start = end_of_period;
|
|
|
|
if(start == end) return;
|
|
|
|
}
|
2020-11-05 23:17:21 +00:00
|
|
|
}
|
2020-11-08 02:10:05 +00:00
|
|
|
|
2020-11-08 03:23:48 +00:00
|
|
|
// Output sync if the moment has arrived.
|
|
|
|
if(end == FinalColumn) {
|
|
|
|
crt_.output_sync(sync_period);
|
|
|
|
}
|
2020-11-05 22:56:20 +00:00
|
|
|
}
|
|
|
|
|
2020-11-17 02:55:41 +00:00
|
|
|
bool VideoBase::get_is_vertical_blank(Cycles offset) {
|
2020-11-08 04:14:50 +00:00
|
|
|
// Cf. http://www.1000bit.it/support/manuali/apple/technotes/iigs/tn.iigs.040.html ;
|
2020-11-17 02:55:41 +00:00
|
|
|
// this bit covers the entire vertical border area, not just the NTSC-sense vertical blank,
|
|
|
|
// and considers the border to begin at 192 even though Super High-res mode is 200 lines.
|
|
|
|
return (cycles_into_frame_ + offset.as<int>())%(Lines * CyclesPerLine) >= FinalPixelLine * CyclesPerLine;
|
2020-11-05 22:56:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void VideoBase::set_new_video(uint8_t new_video) {
|
|
|
|
new_video_ = new_video;
|
|
|
|
}
|
|
|
|
|
|
|
|
uint8_t VideoBase::get_new_video() {
|
|
|
|
return new_video_;
|
|
|
|
}
|
|
|
|
|
|
|
|
void VideoBase::clear_interrupts(uint8_t mask) {
|
|
|
|
set_interrupts(interrupts_ & ~(mask & 0x60));
|
|
|
|
}
|
|
|
|
|
|
|
|
void VideoBase::set_interrupt_register(uint8_t mask) {
|
|
|
|
set_interrupts(interrupts_ | (mask & 0x6));
|
|
|
|
}
|
|
|
|
|
|
|
|
uint8_t VideoBase::get_interrupt_register() {
|
|
|
|
return interrupts_;
|
|
|
|
}
|
|
|
|
|
|
|
|
void VideoBase::notify_clock_tick() {
|
|
|
|
set_interrupts(interrupts_ | 0x40);
|
|
|
|
}
|
|
|
|
|
|
|
|
void VideoBase::set_interrupts(uint8_t new_value) {
|
|
|
|
interrupts_ = new_value & 0x7f;
|
2020-11-06 01:51:00 +00:00
|
|
|
if((interrupts_ >> 4) & interrupts_ & 0x6)
|
2020-11-05 22:56:20 +00:00
|
|
|
interrupts_ |= 0x80;
|
|
|
|
}
|
2020-11-08 03:03:05 +00:00
|
|
|
|
|
|
|
void VideoBase::set_border_colour(uint8_t colour) {
|
2020-11-08 04:14:50 +00:00
|
|
|
border_colour_ = appleii_palette[colour & 0xf];
|
|
|
|
}
|
|
|
|
|
|
|
|
void VideoBase::set_text_colour(uint8_t colour) {
|
|
|
|
text_colour_ = appleii_palette[colour >> 4];
|
|
|
|
background_colour_ = appleii_palette[colour & 0xf];
|
2020-11-08 03:03:05 +00:00
|
|
|
}
|
2020-11-10 02:54:25 +00:00
|
|
|
|
|
|
|
void VideoBase::set_composite_is_colour(bool) {
|
|
|
|
}
|
|
|
|
|
|
|
|
bool VideoBase::get_composite_is_colour() {
|
|
|
|
return true;
|
|
|
|
}
|
2020-11-15 22:16:52 +00:00
|
|
|
|
|
|
|
// MARK: - Outputters.
|
|
|
|
|
2020-11-19 02:06:18 +00:00
|
|
|
uint16_t *VideoBase::output_char(uint16_t *target, uint8_t source, int row) const {
|
2020-11-19 00:47:22 +00:00
|
|
|
const int character = source & character_zones_[source >> 6].address_mask;
|
|
|
|
const uint8_t xor_mask = character_zones_[source >> 6].xor_mask;
|
|
|
|
const std::size_t character_address = size_t(character << 3) + (row & 7);
|
|
|
|
const uint8_t character_pattern = character_rom_[character_address] ^ xor_mask;
|
|
|
|
const uint16_t colours[2] = {background_colour_, text_colour_};
|
|
|
|
|
|
|
|
target[0] = colours[(character_pattern & 0x40) >> 6];
|
|
|
|
target[1] = colours[(character_pattern & 0x20) >> 5];
|
|
|
|
target[2] = colours[(character_pattern & 0x10) >> 4];
|
|
|
|
target[3] = colours[(character_pattern & 0x08) >> 3];
|
|
|
|
target[4] = colours[(character_pattern & 0x04) >> 2];
|
|
|
|
target[5] = colours[(character_pattern & 0x02) >> 1];
|
|
|
|
target[6] = colours[(character_pattern & 0x01) >> 0];
|
|
|
|
return target + 7;
|
|
|
|
}
|
|
|
|
|
2020-11-15 22:16:52 +00:00
|
|
|
uint16_t *VideoBase::output_text(uint16_t *target, int start, int end, int row) const {
|
2020-11-15 23:36:24 +00:00
|
|
|
const uint16_t row_address = get_row_address(row);
|
2020-11-15 22:16:52 +00:00
|
|
|
for(int c = start; c < end; c++) {
|
2020-11-19 00:47:22 +00:00
|
|
|
target = output_char(target, ram_[row_address + c], row);
|
|
|
|
}
|
|
|
|
|
|
|
|
return target;
|
|
|
|
}
|
|
|
|
|
|
|
|
uint16_t *VideoBase::output_double_text(uint16_t *target, int start, int end, int row) const {
|
|
|
|
const uint16_t row_address = get_row_address(row);
|
|
|
|
for(int c = start; c < end; c++) {
|
|
|
|
target = output_char(target, ram_[0x10000 + row_address + c], row);
|
|
|
|
target = output_char(target, ram_[row_address + c], row);
|
2020-11-15 22:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return target;
|
|
|
|
}
|
|
|
|
|
2020-11-15 23:36:24 +00:00
|
|
|
uint16_t *VideoBase::output_super_high_res(uint16_t *target, int start, int end, int row) const {
|
|
|
|
const int row_address = row * 160 + 0x12000;
|
|
|
|
|
|
|
|
// TODO: line_control_ & 0x20 should enable or disable colour fill mode.
|
|
|
|
if(line_control_ & 0x80) {
|
|
|
|
for(int c = start * 4; c < end * 4; c++) {
|
|
|
|
const uint8_t source = ram_[row_address + c];
|
2020-11-20 03:28:10 +00:00
|
|
|
target[0] = palette_[0x8 + ((source >> 6) & 0x3)];
|
|
|
|
target[1] = palette_[0xc + ((source >> 4) & 0x3)];
|
|
|
|
target[2] = palette_[0x0 + ((source >> 2) & 0x3)];
|
|
|
|
target[3] = palette_[0x4 + ((source >> 0) & 0x3)];
|
2020-11-15 23:36:24 +00:00
|
|
|
target += 4;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
for(int c = start * 4; c < end * 4; c++) {
|
|
|
|
const uint8_t source = ram_[row_address + c];
|
|
|
|
target[0] = palette_[(source >> 4) & 0xf];
|
|
|
|
target[1] = palette_[source & 0xf];
|
|
|
|
target += 2;
|
|
|
|
}
|
2020-11-15 22:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return target;
|
|
|
|
}
|
2020-11-21 23:07:51 +00:00
|
|
|
|
|
|
|
uint16_t *VideoBase::output_low_resolution(uint16_t *target, int start, int end, int row) {
|
|
|
|
const int row_shift = row&4;
|
|
|
|
const uint16_t row_address = get_row_address(row);
|
|
|
|
for(int c = start; c < end; c++) {
|
2020-11-22 04:39:58 +00:00
|
|
|
ntsc_shift_ >>= 14;
|
2020-11-21 23:07:51 +00:00
|
|
|
|
|
|
|
const uint8_t source = ram_[row_address + c] >> row_shift;
|
|
|
|
|
2020-11-22 04:39:58 +00:00
|
|
|
// TODO: below is all wrong, now that I've corrected ntsc_shift_ direction.
|
|
|
|
|
2020-11-21 23:07:51 +00:00
|
|
|
// Convulve input as a function of odd/even row.
|
|
|
|
if((start + c)&1) {
|
|
|
|
ntsc_shift_ |= ((source & 4) >> 2) * 0x2222;
|
|
|
|
ntsc_shift_ |= ((source & 8) >> 3) * 0x1111;
|
|
|
|
ntsc_shift_ |= ((source & 1) >> 0) * 0x0888;
|
|
|
|
ntsc_shift_ |= ((source & 2) >> 1) * 0x0444;
|
|
|
|
} else {
|
|
|
|
ntsc_shift_ |= ((source & 1) >> 0) * 0x2222;
|
|
|
|
ntsc_shift_ |= ((source & 2) >> 1) * 0x1111;
|
|
|
|
ntsc_shift_ |= ((source & 4) >> 2) * 0x0888;
|
|
|
|
ntsc_shift_ |= ((source & 8) >> 3) * 0x0444;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: initial state?
|
|
|
|
target = output_shift(target, (c * 14) & 3);
|
|
|
|
}
|
|
|
|
|
|
|
|
return target;
|
|
|
|
}
|
|
|
|
|
|
|
|
uint16_t *VideoBase::output_high_resolution(uint16_t *target, int start, int end, int row) {
|
|
|
|
const uint16_t row_address = get_row_address(row);
|
|
|
|
for(int c = start; c < end; c++) {
|
2020-11-22 04:39:58 +00:00
|
|
|
ntsc_shift_ >>= 14;
|
2020-11-21 23:07:51 +00:00
|
|
|
|
|
|
|
uint8_t source = ram_[row_address + c];
|
|
|
|
|
|
|
|
// TODO: can do this in two multiplies, I think.
|
|
|
|
const uint16_t doubled_source =
|
|
|
|
((source&0x01) * (0x0003 >> 0)) +
|
|
|
|
((source&0x02) * (0x000c >> 1)) +
|
|
|
|
((source&0x04) * (0x0030 >> 2)) +
|
|
|
|
((source&0x08) * (0x00c0 >> 3)) +
|
|
|
|
((source&0x10) * (0x0300 >> 4)) +
|
|
|
|
((source&0x20) * (0x0c00 >> 5)) +
|
|
|
|
((source&0x40) * (0x3000 >> 6));
|
|
|
|
|
|
|
|
// Just append new bits, doubled up (and possibly delayed).
|
2020-11-22 04:39:58 +00:00
|
|
|
// TODO: I can kill the conditional here. Probably?
|
|
|
|
// if(source & high_resolution_mask_ & 0x80) {
|
|
|
|
// ntsc_shift_ |= doubled_source << 5;
|
|
|
|
// } else {
|
|
|
|
// ntsc_shift_ = (ntsc_shift_ & 0xf) | (doubled_source << 4) | ((doubled_source << 5) & 0x80000);
|
|
|
|
// }
|
|
|
|
|
|
|
|
// TODO: reintroduce delay bit. Delay bit should: (i) hold existing bit; and (ii) store top bit for potential
|
|
|
|
// delay into the next word.
|
|
|
|
ntsc_shift_ |= doubled_source << 4;
|
2020-11-21 23:07:51 +00:00
|
|
|
|
|
|
|
// TODO: initial state?
|
2020-11-22 03:53:26 +00:00
|
|
|
target = output_shift(target, (2 + (c * 14)) & 3);
|
2020-11-21 23:07:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return target;
|
|
|
|
}
|
|
|
|
|
|
|
|
uint16_t *VideoBase::output_shift(uint16_t *target, int phase) const {
|
|
|
|
constexpr uint8_t rolls[4][16] = {
|
|
|
|
{
|
|
|
|
0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf
|
|
|
|
},
|
|
|
|
{
|
|
|
|
0x0, 0x8, 0x1, 0x9, 0x2, 0xa, 0x3, 0xb,
|
|
|
|
0x4, 0xc, 0x5, 0xd, 0x6, 0xe, 0x7, 0xf
|
|
|
|
},
|
|
|
|
{
|
|
|
|
0x0, 0x4, 0x8, 0xc, 0x1, 0x5, 0x9, 0xd,
|
|
|
|
0x2, 0x6, 0xa, 0xe, 0x3, 0x7, 0xb, 0xf
|
|
|
|
},
|
|
|
|
{
|
|
|
|
0x0, 0x2, 0x4, 0x6, 0x8, 0xa, 0xc, 0xe,
|
|
|
|
0x1, 0x3, 0x5, 0x7, 0x9, 0xb, 0xd, 0xf
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
#define OutputPixel(offset) {\
|
|
|
|
const auto phase_offset = ntsc_shift_lookup_[(ntsc_shift_ >> offset) & 0xff]; \
|
|
|
|
const auto raw_bits = (ntsc_shift_ >> (offset + phase_offset)) & 0x0f; \
|
2020-11-22 04:39:58 +00:00
|
|
|
target[offset] = appleii_palette[rolls[(phase + offset + phase_offset)&3][raw_bits]]; \
|
2020-11-21 23:07:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
OutputPixel(0);
|
2020-11-22 04:39:58 +00:00
|
|
|
OutputPixel(1);
|
|
|
|
OutputPixel(2);
|
|
|
|
OutputPixel(3);
|
|
|
|
OutputPixel(4);
|
|
|
|
OutputPixel(5);
|
|
|
|
OutputPixel(6);
|
|
|
|
OutputPixel(7);
|
|
|
|
OutputPixel(8);
|
|
|
|
OutputPixel(9);
|
|
|
|
OutputPixel(10);
|
|
|
|
OutputPixel(11);
|
|
|
|
OutputPixel(12);
|
|
|
|
OutputPixel(13);
|
2020-11-21 23:07:51 +00:00
|
|
|
|
|
|
|
#undef OutputPixel
|
|
|
|
|
|
|
|
return target + 14;
|
|
|
|
}
|