2016-10-12 23:20:23 +00:00
|
|
|
//
|
|
|
|
// Video.cpp
|
|
|
|
// Clock Signal
|
|
|
|
//
|
|
|
|
// Created by Thomas Harte on 12/10/2016.
|
2018-05-13 19:19:52 +00:00
|
|
|
// Copyright 2016 Thomas Harte. All rights reserved.
|
2016-10-12 23:20:23 +00:00
|
|
|
//
|
|
|
|
|
|
|
|
#include "Video.hpp"
|
|
|
|
|
2018-11-04 01:54:25 +00:00
|
|
|
#include <algorithm>
|
|
|
|
|
2019-02-19 01:49:54 +00:00
|
|
|
//#define SUPPLY_COMPOSITE
|
2019-02-18 22:20:52 +00:00
|
|
|
|
2016-10-12 23:20:23 +00:00
|
|
|
using namespace Oric;
|
|
|
|
|
2016-10-20 11:34:23 +00:00
|
|
|
namespace {
|
|
|
|
const unsigned int PAL50VSyncStartPosition = 256*64;
|
|
|
|
const unsigned int PAL60VSyncStartPosition = 234*64;
|
|
|
|
const unsigned int PAL50VSyncEndPosition = 259*64;
|
|
|
|
const unsigned int PAL60VSyncEndPosition = 238*64;
|
|
|
|
const unsigned int PAL50Period = 312*64;
|
|
|
|
const unsigned int PAL60Period = 262*64;
|
|
|
|
}
|
|
|
|
|
2018-11-29 02:16:13 +00:00
|
|
|
VideoOutput::VideoOutput(uint8_t *memory) :
|
|
|
|
ram_(memory),
|
2018-11-30 00:29:28 +00:00
|
|
|
crt_(64*6, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red1Green1Blue1),
|
2020-01-24 03:14:02 +00:00
|
|
|
frequency_mismatch_warner_(*this),
|
2018-11-29 02:16:13 +00:00
|
|
|
v_sync_start_position_(PAL50VSyncStartPosition), v_sync_end_position_(PAL50VSyncEndPosition),
|
|
|
|
counter_period_(PAL50Period) {
|
2018-11-30 00:29:28 +00:00
|
|
|
crt_.set_phase_linked_luminance_offset(-1.0f / 8.0f);
|
2019-02-18 21:56:48 +00:00
|
|
|
data_type_ = Outputs::Display::InputDataType::Red1Green1Blue1;
|
2019-02-18 22:20:52 +00:00
|
|
|
crt_.set_input_data_type(data_type_);
|
2020-01-24 03:14:02 +00:00
|
|
|
crt_.set_delegate(&frequency_mismatch_warner_);
|
|
|
|
update_crt_frequency();
|
|
|
|
}
|
|
|
|
|
|
|
|
void VideoOutput::register_crt_frequency_mismatch() {
|
|
|
|
crt_is_60Hz_ ^= true;
|
|
|
|
update_crt_frequency();
|
|
|
|
}
|
|
|
|
|
|
|
|
void VideoOutput::update_crt_frequency() {
|
|
|
|
// Set the proper frequency...
|
|
|
|
crt_.set_new_display_type(64*6, crt_is_60Hz_ ? Outputs::Display::Type::PAL60 : Outputs::Display::Type::PAL50);
|
|
|
|
|
|
|
|
// ... but also pick an appropriate crop rectangle.
|
|
|
|
crt_.set_visible_area(crt_.get_rect_for_area(crt_is_60Hz_ ? 26 : 54, 224, 16 * 6, 40 * 6, 4.0f / 3.0f));
|
2016-10-12 23:20:23 +00:00
|
|
|
}
|
|
|
|
|
2018-11-29 01:53:33 +00:00
|
|
|
void VideoOutput::set_display_type(Outputs::Display::DisplayType display_type) {
|
2019-02-18 22:20:52 +00:00
|
|
|
crt_.set_display_type(display_type);
|
|
|
|
|
|
|
|
#ifdef SUPPLY_COMPOSITE
|
2019-02-18 21:56:48 +00:00
|
|
|
const auto data_type =
|
2021-06-07 02:43:53 +00:00
|
|
|
(!has_colour_rom_ || display_type == Outputs::Display::DisplayType::RGB) ?
|
2019-02-18 21:56:48 +00:00
|
|
|
Outputs::Display::InputDataType::Red1Green1Blue1 :
|
|
|
|
Outputs::Display::InputDataType::PhaseLinkedLuminance8;
|
2019-02-18 22:20:52 +00:00
|
|
|
#else
|
|
|
|
const auto data_type = Outputs::Display::InputDataType::Red1Green1Blue1;
|
|
|
|
#endif
|
|
|
|
|
2019-02-18 21:56:48 +00:00
|
|
|
if(data_type_ != data_type) {
|
|
|
|
data_type_ = data_type;
|
|
|
|
crt_.set_input_data_type(data_type_);
|
2018-11-30 00:29:28 +00:00
|
|
|
}
|
2018-11-29 02:16:13 +00:00
|
|
|
}
|
|
|
|
|
2020-05-21 03:34:26 +00:00
|
|
|
Outputs::Display::DisplayType VideoOutput::get_display_type() const {
|
2020-03-18 22:31:31 +00:00
|
|
|
return crt_.get_display_type();
|
|
|
|
}
|
|
|
|
|
2018-11-29 02:16:13 +00:00
|
|
|
void VideoOutput::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
|
|
|
|
crt_.set_scan_target(scan_target);
|
2016-12-10 18:55:56 +00:00
|
|
|
}
|
|
|
|
|
2020-01-22 03:28:25 +00:00
|
|
|
Outputs::Display::ScanStatus VideoOutput::get_scaled_scan_status() const {
|
|
|
|
return crt_.get_scaled_scan_status() / 6.0f;
|
2020-01-21 02:45:10 +00:00
|
|
|
}
|
|
|
|
|
2017-03-26 18:34:47 +00:00
|
|
|
void VideoOutput::set_colour_rom(const std::vector<uint8_t> &rom) {
|
2021-06-07 02:43:53 +00:00
|
|
|
has_colour_rom_ = true;
|
2017-11-11 20:28:40 +00:00
|
|
|
for(std::size_t c = 0; c < 8; c++) {
|
2018-11-29 04:40:22 +00:00
|
|
|
colour_forms_[c] = 0;
|
|
|
|
|
|
|
|
uint8_t *const colour = reinterpret_cast<uint8_t *>(&colour_forms_[c]);
|
|
|
|
const std::size_t index = (c << 2);
|
|
|
|
|
|
|
|
// Values in the ROM are encoded for indexing by two square waves
|
|
|
|
// in quadrature, which means that they're indexed in the order
|
|
|
|
// 0, 1, 3, 2.
|
|
|
|
colour[0] = uint8_t((rom[index] & 0x0f) << 4);
|
2019-02-19 01:21:41 +00:00
|
|
|
colour[1] = uint8_t(rom[index] & 0xf0);
|
2018-11-29 04:40:22 +00:00
|
|
|
colour[2] = uint8_t(rom[index+1] & 0xf0);
|
2019-02-19 01:21:41 +00:00
|
|
|
colour[3] = uint8_t((rom[index+1] & 0x0f) << 4);
|
2018-11-29 04:40:22 +00:00
|
|
|
|
|
|
|
// Extracting just the visible part of the stored range of values
|
2019-02-19 01:49:54 +00:00
|
|
|
// means extracting the range 0x40 to 0xe0.
|
2018-11-29 04:40:22 +00:00
|
|
|
for(int sub = 0; sub < 4; ++sub) {
|
|
|
|
colour[sub] = ((colour[sub] - 0x40) * 255) / 0xa0;
|
|
|
|
}
|
2016-12-10 03:17:10 +00:00
|
|
|
}
|
2016-12-11 00:10:33 +00:00
|
|
|
|
2018-11-29 04:40:22 +00:00
|
|
|
// Check for big endianness and byte swap if required.
|
2019-02-19 01:21:41 +00:00
|
|
|
// uint32_t test_value = 0x0001;
|
|
|
|
// if(*reinterpret_cast<uint8_t *>(&test_value) != 0x01) {
|
2018-11-29 04:40:22 +00:00
|
|
|
// for(std::size_t c = 0; c < 8; c++) {
|
2020-05-10 03:00:39 +00:00
|
|
|
// colour_forms_[c] = uint16_t((colour_forms_[c] >> 8) | (colour_forms_[c] << 8));
|
2018-11-29 04:40:22 +00:00
|
|
|
// }
|
2019-02-19 01:21:41 +00:00
|
|
|
// }
|
2016-12-10 03:17:10 +00:00
|
|
|
}
|
|
|
|
|
2017-07-28 02:05:29 +00:00
|
|
|
void VideoOutput::run_for(const Cycles cycles) {
|
2020-01-24 03:14:02 +00:00
|
|
|
// Horizontal: 0-39: pixels; otherwise blank; 48-53 sync, 54-56 colour burst.
|
|
|
|
// Vertical: 0-223: pixels; otherwise blank; 256-259 (50Hz) or 234-238 (60Hz) sync.
|
2016-10-13 01:29:21 +00:00
|
|
|
|
2016-10-30 20:21:20 +00:00
|
|
|
#define clamp(action) \
|
|
|
|
if(cycles_run_for <= number_of_cycles) { action; } else cycles_run_for = number_of_cycles;
|
|
|
|
|
2019-10-30 02:36:29 +00:00
|
|
|
int number_of_cycles = int(cycles.as_integral());
|
2017-03-26 18:34:47 +00:00
|
|
|
while(number_of_cycles) {
|
2016-11-22 14:28:45 +00:00
|
|
|
int h_counter = counter_ & 63;
|
2016-10-30 19:30:39 +00:00
|
|
|
int cycles_run_for = 0;
|
2016-10-13 11:59:11 +00:00
|
|
|
|
2017-03-26 18:34:47 +00:00
|
|
|
if(counter_ >= v_sync_start_position_ && counter_ < v_sync_end_position_) {
|
2016-10-30 19:30:39 +00:00
|
|
|
// this is a sync line
|
2016-11-22 14:28:45 +00:00
|
|
|
cycles_run_for = v_sync_end_position_ - counter_;
|
2018-11-29 02:16:13 +00:00
|
|
|
clamp(crt_.output_sync((v_sync_end_position_ - v_sync_start_position_) * 6));
|
2017-03-26 18:34:47 +00:00
|
|
|
} else if(counter_ < 224*64 && h_counter < 40) {
|
2016-10-30 19:30:39 +00:00
|
|
|
// this is a pixel line
|
2017-03-26 18:34:47 +00:00
|
|
|
if(!h_counter) {
|
2016-12-10 20:19:48 +00:00
|
|
|
ink_ = 0x7;
|
2016-12-10 19:17:46 +00:00
|
|
|
paper_ = 0x0;
|
2016-11-22 14:28:45 +00:00
|
|
|
use_alternative_character_set_ = use_double_height_characters_ = blink_text_ = false;
|
2016-10-30 19:30:39 +00:00
|
|
|
set_character_set_base_address();
|
2018-11-29 02:40:43 +00:00
|
|
|
|
2019-02-18 21:56:48 +00:00
|
|
|
if(data_type_ == Outputs::Display::InputDataType::Red1Green1Blue1) {
|
|
|
|
rgb_pixel_target_ = reinterpret_cast<uint8_t *>(crt_.begin_data(240));
|
|
|
|
} else {
|
|
|
|
composite_pixel_target_ = reinterpret_cast<uint32_t *>(crt_.begin_data(240));
|
|
|
|
}
|
2016-10-30 19:30:39 +00:00
|
|
|
|
2017-03-26 18:34:47 +00:00
|
|
|
if(!counter_) {
|
2016-11-22 14:28:45 +00:00
|
|
|
frame_counter_++;
|
2016-10-20 11:34:23 +00:00
|
|
|
|
2016-11-22 14:28:45 +00:00
|
|
|
v_sync_start_position_ = next_frame_is_sixty_hertz_ ? PAL60VSyncStartPosition : PAL50VSyncStartPosition;
|
|
|
|
v_sync_end_position_ = next_frame_is_sixty_hertz_ ? PAL60VSyncEndPosition : PAL50VSyncEndPosition;
|
|
|
|
counter_period_ = next_frame_is_sixty_hertz_ ? PAL60Period : PAL50Period;
|
2016-10-30 19:30:39 +00:00
|
|
|
}
|
2016-10-17 02:14:01 +00:00
|
|
|
}
|
2016-10-13 01:29:21 +00:00
|
|
|
|
2016-10-30 19:30:39 +00:00
|
|
|
cycles_run_for = std::min(40 - h_counter, number_of_cycles);
|
|
|
|
int columns = cycles_run_for;
|
2016-11-22 14:28:45 +00:00
|
|
|
int pixel_base_address = 0xa000 + (counter_ >> 6) * 40;
|
|
|
|
int character_base_address = 0xbb80 + (counter_ >> 9) * 40;
|
2019-02-19 01:21:41 +00:00
|
|
|
const uint8_t blink_mask = (blink_text_ && (frame_counter_&32)) ? 0x00 : 0xff;
|
2016-10-28 01:06:31 +00:00
|
|
|
|
2017-03-26 18:34:47 +00:00
|
|
|
while(columns--) {
|
2016-10-30 19:30:39 +00:00
|
|
|
uint8_t pixels, control_byte;
|
2016-10-13 01:29:21 +00:00
|
|
|
|
2017-03-26 18:34:47 +00:00
|
|
|
if(is_graphics_mode_ && counter_ < 200*64) {
|
2016-11-22 14:28:45 +00:00
|
|
|
control_byte = pixels = ram_[pixel_base_address + h_counter];
|
2017-03-26 18:34:47 +00:00
|
|
|
} else {
|
2016-10-30 20:21:20 +00:00
|
|
|
int address = character_base_address + h_counter;
|
2016-11-22 14:28:45 +00:00
|
|
|
control_byte = ram_[address];
|
2019-02-19 01:21:41 +00:00
|
|
|
const int line = use_double_height_characters_ ? ((counter_ >> 7) & 7) : ((counter_ >> 6) & 7);
|
2016-11-22 14:28:45 +00:00
|
|
|
pixels = ram_[character_set_base_address_ + (control_byte&127) * 8 + line];
|
2016-10-30 19:30:39 +00:00
|
|
|
}
|
|
|
|
|
2019-02-19 01:21:41 +00:00
|
|
|
const uint8_t inverse_mask = (control_byte & 0x80) ? 0x7 : 0x0;
|
2016-10-30 20:21:20 +00:00
|
|
|
pixels &= blink_mask;
|
2016-10-30 19:30:39 +00:00
|
|
|
|
2017-03-26 18:34:47 +00:00
|
|
|
if(control_byte & 0x60) {
|
2019-02-18 21:56:48 +00:00
|
|
|
if(data_type_ == Outputs::Display::InputDataType::Red1Green1Blue1 && rgb_pixel_target_) {
|
2018-11-29 02:40:43 +00:00
|
|
|
const uint8_t colours[2] = {
|
|
|
|
uint8_t(paper_ ^ inverse_mask),
|
|
|
|
uint8_t(ink_ ^ inverse_mask)
|
|
|
|
};
|
|
|
|
rgb_pixel_target_[0] = colours[(pixels >> 5)&1];
|
|
|
|
rgb_pixel_target_[1] = colours[(pixels >> 4)&1];
|
|
|
|
rgb_pixel_target_[2] = colours[(pixels >> 3)&1];
|
|
|
|
rgb_pixel_target_[3] = colours[(pixels >> 2)&1];
|
|
|
|
rgb_pixel_target_[4] = colours[(pixels >> 1)&1];
|
|
|
|
rgb_pixel_target_[5] = colours[(pixels >> 0)&1];
|
2019-02-18 21:56:48 +00:00
|
|
|
} else if(composite_pixel_target_) {
|
2018-11-29 02:40:43 +00:00
|
|
|
const uint32_t colours[2] = {
|
|
|
|
colour_forms_[paper_ ^ inverse_mask],
|
|
|
|
colour_forms_[ink_ ^ inverse_mask]
|
|
|
|
};
|
|
|
|
composite_pixel_target_[0] = colours[(pixels >> 5)&1];
|
|
|
|
composite_pixel_target_[1] = colours[(pixels >> 4)&1];
|
|
|
|
composite_pixel_target_[2] = colours[(pixels >> 3)&1];
|
|
|
|
composite_pixel_target_[3] = colours[(pixels >> 2)&1];
|
|
|
|
composite_pixel_target_[4] = colours[(pixels >> 1)&1];
|
|
|
|
composite_pixel_target_[5] = colours[(pixels >> 0)&1];
|
2016-10-30 19:30:39 +00:00
|
|
|
}
|
2017-03-26 18:34:47 +00:00
|
|
|
} else {
|
|
|
|
switch(control_byte & 0x1f) {
|
2016-12-10 19:17:46 +00:00
|
|
|
case 0x00: ink_ = 0x0; break;
|
|
|
|
case 0x01: ink_ = 0x4; break;
|
|
|
|
case 0x02: ink_ = 0x2; break;
|
|
|
|
case 0x03: ink_ = 0x6; break;
|
|
|
|
case 0x04: ink_ = 0x1; break;
|
|
|
|
case 0x05: ink_ = 0x5; break;
|
|
|
|
case 0x06: ink_ = 0x3; break;
|
|
|
|
case 0x07: ink_ = 0x7; break;
|
2016-10-30 19:30:39 +00:00
|
|
|
|
|
|
|
case 0x08: case 0x09: case 0x0a: case 0x0b:
|
|
|
|
case 0x0c: case 0x0d: case 0x0e: case 0x0f:
|
2016-11-22 14:28:45 +00:00
|
|
|
use_alternative_character_set_ = (control_byte&1);
|
|
|
|
use_double_height_characters_ = (control_byte&2);
|
|
|
|
blink_text_ = (control_byte&4);
|
2016-10-30 19:30:39 +00:00
|
|
|
set_character_set_base_address();
|
|
|
|
break;
|
|
|
|
|
2016-12-10 19:17:46 +00:00
|
|
|
case 0x10: paper_ = 0x0; break;
|
|
|
|
case 0x11: paper_ = 0x4; break;
|
|
|
|
case 0x12: paper_ = 0x2; break;
|
|
|
|
case 0x13: paper_ = 0x6; break;
|
|
|
|
case 0x14: paper_ = 0x1; break;
|
|
|
|
case 0x15: paper_ = 0x5; break;
|
|
|
|
case 0x16: paper_ = 0x3; break;
|
|
|
|
case 0x17: paper_ = 0x7; break;
|
2016-10-30 19:30:39 +00:00
|
|
|
|
|
|
|
case 0x18: case 0x19: case 0x1a: case 0x1b:
|
|
|
|
case 0x1c: case 0x1d: case 0x1e: case 0x1f:
|
2016-11-22 14:28:45 +00:00
|
|
|
is_graphics_mode_ = (control_byte & 4);
|
|
|
|
next_frame_is_sixty_hertz_ = !(control_byte & 2);
|
2016-10-30 19:30:39 +00:00
|
|
|
break;
|
|
|
|
|
|
|
|
default: break;
|
|
|
|
}
|
2018-11-29 02:40:43 +00:00
|
|
|
|
2019-02-18 21:56:48 +00:00
|
|
|
if(data_type_ == Outputs::Display::InputDataType::Red1Green1Blue1 && rgb_pixel_target_) {
|
2018-11-29 02:40:43 +00:00
|
|
|
rgb_pixel_target_[0] = rgb_pixel_target_[1] =
|
|
|
|
rgb_pixel_target_[2] = rgb_pixel_target_[3] =
|
|
|
|
rgb_pixel_target_[4] = rgb_pixel_target_[5] = paper_ ^ inverse_mask;
|
2019-02-18 21:56:48 +00:00
|
|
|
} else if(composite_pixel_target_) {
|
2018-11-29 02:40:43 +00:00
|
|
|
composite_pixel_target_[0] = composite_pixel_target_[1] =
|
|
|
|
composite_pixel_target_[2] = composite_pixel_target_[3] =
|
|
|
|
composite_pixel_target_[4] = composite_pixel_target_[5] = colour_forms_[paper_ ^ inverse_mask];
|
|
|
|
}
|
2016-10-30 19:30:39 +00:00
|
|
|
}
|
2018-11-29 02:40:43 +00:00
|
|
|
if(rgb_pixel_target_) rgb_pixel_target_ += 6;
|
|
|
|
if(composite_pixel_target_) composite_pixel_target_ += 6;
|
2016-10-30 19:30:39 +00:00
|
|
|
h_counter++;
|
|
|
|
}
|
|
|
|
|
2017-03-26 18:34:47 +00:00
|
|
|
if(h_counter == 40) {
|
2018-11-29 02:16:13 +00:00
|
|
|
crt_.output_data(40 * 6);
|
2018-11-29 02:40:43 +00:00
|
|
|
rgb_pixel_target_ = nullptr;
|
|
|
|
composite_pixel_target_ = nullptr;
|
2016-10-13 01:29:21 +00:00
|
|
|
}
|
2017-03-26 18:34:47 +00:00
|
|
|
} else {
|
2016-10-30 19:30:39 +00:00
|
|
|
// this is a blank line (or the equivalent part of a pixel line)
|
2017-03-26 18:34:47 +00:00
|
|
|
if(h_counter < 48) {
|
2016-10-30 20:21:20 +00:00
|
|
|
cycles_run_for = 48 - h_counter;
|
|
|
|
clamp(
|
2016-11-22 14:28:45 +00:00
|
|
|
int period = (counter_ < 224*64) ? 8 : 48;
|
2018-11-29 02:16:13 +00:00
|
|
|
crt_.output_blank(period * 6);
|
2016-10-30 20:21:20 +00:00
|
|
|
);
|
2017-03-26 18:34:47 +00:00
|
|
|
} else if(h_counter < 54) {
|
2016-10-30 20:21:20 +00:00
|
|
|
cycles_run_for = 54 - h_counter;
|
2018-11-29 02:16:13 +00:00
|
|
|
clamp(crt_.output_sync(6 * 6));
|
2017-03-26 18:34:47 +00:00
|
|
|
} else if(h_counter < 56) {
|
2016-10-30 20:21:20 +00:00
|
|
|
cycles_run_for = 56 - h_counter;
|
2018-11-29 02:16:13 +00:00
|
|
|
clamp(crt_.output_default_colour_burst(2 * 6));
|
2017-03-26 18:34:47 +00:00
|
|
|
} else {
|
2016-10-30 20:21:20 +00:00
|
|
|
cycles_run_for = 64 - h_counter;
|
2018-11-29 02:16:13 +00:00
|
|
|
clamp(crt_.output_blank(8 * 6));
|
2016-10-13 01:52:47 +00:00
|
|
|
}
|
2016-10-13 01:29:21 +00:00
|
|
|
}
|
2016-10-30 19:30:39 +00:00
|
|
|
|
2016-11-22 14:28:45 +00:00
|
|
|
counter_ = (counter_ + cycles_run_for)%counter_period_;
|
2016-10-30 19:30:39 +00:00
|
|
|
number_of_cycles -= cycles_run_for;
|
2016-10-13 01:29:21 +00:00
|
|
|
}
|
2016-10-12 23:20:23 +00:00
|
|
|
}
|
2016-10-31 00:16:22 +00:00
|
|
|
|
2017-03-26 18:34:47 +00:00
|
|
|
void VideoOutput::set_character_set_base_address() {
|
2016-11-22 14:28:45 +00:00
|
|
|
if(is_graphics_mode_) character_set_base_address_ = use_alternative_character_set_ ? 0x9c00 : 0x9800;
|
|
|
|
else character_set_base_address_ = use_alternative_character_set_ ? 0xb800 : 0xb400;
|
2016-10-31 00:16:22 +00:00
|
|
|
}
|