1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-04-06 10:38:16 +00:00

Merge pull request #581 from TomHarte/ScanTarget

Decouples output of raster scans from their generation
This commit is contained in:
Thomas Harte 2019-02-27 18:52:19 -05:00 committed by GitHub
commit c8c24f81c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 3897 additions and 4046 deletions

View File

@ -48,27 +48,16 @@ void MultiCRTMachine::perform_parallel(const std::function<void(::CRTMachine::Ma
void MultiCRTMachine::perform_serial(const std::function<void (::CRTMachine::Machine *)> &function) {
std::lock_guard<std::mutex> machines_lock(machines_mutex_);
for(const auto &machine: machines_) {
CRTMachine::Machine *crt_machine = machine->crt_machine();
CRTMachine::Machine *const crt_machine = machine->crt_machine();
if(crt_machine) function(crt_machine);
}
}
void MultiCRTMachine::setup_output(float aspect_ratio) {
perform_serial([=](::CRTMachine::Machine *machine) {
machine->setup_output(aspect_ratio);
});
}
void MultiCRTMachine::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
scan_target_ = scan_target;
void MultiCRTMachine::close_output() {
perform_serial([=](::CRTMachine::Machine *machine) {
machine->close_output();
});
}
Outputs::CRT::CRT *MultiCRTMachine::get_crt() {
std::lock_guard<std::mutex> machines_lock(machines_mutex_);
CRTMachine::Machine *crt_machine = machines_.front()->crt_machine();
return crt_machine ? crt_machine->get_crt() : nullptr;
CRTMachine::Machine *const crt_machine = machines_.front()->crt_machine();
if(crt_machine) crt_machine->set_scan_target(scan_target);
}
Outputs::Speaker::Speaker *MultiCRTMachine::get_speaker() {
@ -84,6 +73,14 @@ void MultiCRTMachine::run_for(Time::Seconds duration) {
}
void MultiCRTMachine::did_change_machine_order() {
if(scan_target_) scan_target_->will_change_owner();
perform_serial([=](::CRTMachine::Machine *machine) {
machine->set_scan_target(nullptr);
});
CRTMachine::Machine *const crt_machine = machines_.front()->crt_machine();
if(crt_machine) crt_machine->set_scan_target(scan_target_);
if(speaker_) {
speaker_->set_new_front_machine(machines_.front().get());
}

View File

@ -53,9 +53,7 @@ class MultiCRTMachine: public CRTMachine::Machine {
}
// Below is the standard CRTMachine::Machine interface; see there for documentation.
void setup_output(float aspect_ratio) override;
void close_output() override;
Outputs::CRT::CRT *get_crt() override;
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override;
Outputs::Speaker::Speaker *get_speaker() override;
void run_for(Time::Seconds duration) override;
@ -66,6 +64,7 @@ class MultiCRTMachine: public CRTMachine::Machine {
std::vector<Concurrency::AsyncTaskQueue> queues_;
MultiSpeaker *speaker_ = nullptr;
Delegate *delegate_ = nullptr;
Outputs::Display::ScanTarget *scan_target_ = nullptr;
/*!
Performs a parallel for operation across all machines, performing the supplied

View File

@ -11,7 +11,7 @@
using namespace Analyser::Dynamic;
MultiKeyboardMachine::MultiKeyboardMachine(const std::vector<std::unique_ptr<::Machine::DynamicMachine>> &machines) :
keyboard_(machines_) {
keyboard_(machines_) {
for(const auto &machine: machines) {
KeyboardMachine::Machine *keyboard_machine = machine->keyboard_machine();
if(keyboard_machine) machines_.push_back(keyboard_machine);

View File

@ -80,7 +80,7 @@ Analyser::Static::TargetList Analyser::Static::Commodore::GetTargets(const Media
target->memory_model = Target::MemoryModel::Unexpanded;
std::ostringstream string_stream;
string_stream << "LOAD\"" << (is_disk ? "*" : "") << "\"," << device << ",";
if(files.front().is_basic()) {
if(files.front().is_basic()) {
string_stream << "0";
} else {
string_stream << "1";

View File

@ -63,9 +63,9 @@ class Accessor {
#define z(v) (v & 7)
Instruction::Condition condition_table[] = {
Instruction::Condition::NZ, Instruction::Condition::Z,
Instruction::Condition::NC, Instruction::Condition::C,
Instruction::Condition::PO, Instruction::Condition::PE,
Instruction::Condition::NZ, Instruction::Condition::Z,
Instruction::Condition::NC, Instruction::Condition::C,
Instruction::Condition::PO, Instruction::Condition::PE,
Instruction::Condition::P, Instruction::Condition::M
};

View File

@ -9,6 +9,7 @@
#ifndef ClockDeferrer_h
#define ClockDeferrer_h
#include <functional>
#include <vector>
/*!

View File

@ -189,7 +189,6 @@ void WD1770::posit_event(int new_event_type) {
interesting_event_mask_ &= ~new_event_type;
}
Status new_status;
BEGIN_SECTION()
// Wait for a new command, branch to the appropriate handler.
@ -211,7 +210,7 @@ void WD1770::posit_event(int new_event_type) {
status.interrupt_request = false;
});
LOG("Starting " << std::hex << command_ << std::endl);
LOG("Starting " << PADHEX(2) << int(command_));
if(!(command_ & 0x80)) goto begin_type_1;
if(!(command_ & 0x40)) goto begin_type_2;
@ -329,7 +328,7 @@ void WD1770::posit_event(int new_event_type) {
}
if(header_[0] == track_) {
LOG("Reached track " << std::dec << track_);
LOG("Reached track " << std::dec << int(track_));
update_status([] (Status &status) {
status.crc_error = false;
});
@ -398,18 +397,18 @@ void WD1770::posit_event(int new_event_type) {
READ_ID();
if(index_hole_count_ == 5) {
LOG("Failed to find sector " << std::dec << sector_);
LOG("Failed to find sector " << std::dec << int(sector_));
update_status([] (Status &status) {
status.record_not_found = true;
});
goto wait_for_command;
}
if(distance_into_section_ == 7) {
LOG("Considering " << std::dec << header_[0] << "/" << header_[2]);
LOG("Considering " << std::dec << int(header_[0]) << "/" << int(header_[2]));
set_data_mode(DataMode::Scanning);
if( header_[0] == track_ && header_[2] == sector_ &&
(has_motor_on_line() || !(command_&0x02) || ((command_&0x08) >> 3) == header_[1])) {
LOG("Found " << std::dec << header_[0] << "/" << header_[2]);
LOG("Found " << std::dec << int(header_[0]) << "/" << int(header_[2]));
if(get_crc_generator().get_value()) {
LOG("CRC error; back to searching");
update_status([] (Status &status) {
@ -478,7 +477,7 @@ void WD1770::posit_event(int new_event_type) {
sector_++;
goto test_type2_write_protection;
}
LOG("Finished reading sector " << std::dec << sector_);
LOG("Finished reading sector " << std::dec << int(sector_));
goto wait_for_command;
}
goto type2_check_crc;
@ -560,7 +559,7 @@ void WD1770::posit_event(int new_event_type) {
sector_++;
goto test_type2_write_protection;
}
LOG("Wrote sector " << std::dec << sector_);
LOG("Wrote sector " << std::dec << int(sector_));
goto wait_for_command;

View File

@ -34,7 +34,7 @@ class AudioGenerator: public ::Outputs::Speaker::SampleSource {
private:
Concurrency::DeferringAsyncTaskQueue &audio_queue_;
unsigned int counters_[4] = {2, 1, 0, 0}; // create a slight phase offset for the three channels
unsigned int counters_[4] = {2, 1, 0, 0}; // create a slight phase offset for the three channels
unsigned int shift_registers_[4] = {0, 0, 0, 0};
uint8_t control_registers_[4] = {0, 0, 0, 0};
int16_t volume_ = 0;
@ -64,23 +64,12 @@ template <class BusHandler> class MOS6560 {
public:
MOS6560(BusHandler &bus_handler) :
bus_handler_(bus_handler),
crt_(new Outputs::CRT::CRT(65*4, 4, Outputs::CRT::DisplayType::NTSC60, 2)),
crt_(65*4, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Luminance8Phase8),
audio_generator_(audio_queue_),
speaker_(audio_generator_)
{
crt_->set_svideo_sampling_function(
"vec2 svideo_sample(usampler2D texID, vec2 coordinate, float phase, float amplitude)"
"{"
"vec2 yc = texture(texID, coordinate).rg / vec2(255.0);"
"float phaseOffset = 6.283185308 * 2.0 * yc.y;"
"float chroma = step(yc.y, 0.75) * cos(phase + phaseOffset);"
"return vec2(yc.x, chroma);"
"}");
// default to s-video output
crt_->set_video_signal(Outputs::CRT::VideoSignal::SVideo);
crt_.set_display_type(Outputs::Display::DisplayType::SVideo);
// default to NTSC
set_output_mode(OutputMode::NTSC);
@ -94,7 +83,8 @@ template <class BusHandler> class MOS6560 {
speaker_.set_input_rate(static_cast<float>(clock_rate / 4.0));
}
Outputs::CRT::CRT *get_crt() { return crt_.get(); }
void set_scan_target(Outputs::Display::ScanTarget *scan_target) { crt_.set_scan_target(scan_target); }
void set_display_type(Outputs::Display::DisplayType display_type) { crt_.set_display_type(display_type); }
Outputs::Speaker::Speaker *get_speaker() { return &speaker_; }
void set_high_frequency_cutoff(float cutoff) {
@ -117,12 +107,12 @@ template <class BusHandler> class MOS6560 {
// Chrominances are encoded such that 0-128 is a complete revolution of phase;
// anything above 191 disables the colour subcarrier. Phase is relative to the
// colour burst, so 0 is green.
// colour burst, so 0 is green (NTSC) or blue/violet (PAL).
const uint8_t pal_chrominances[16] = {
255, 255, 37, 101,
19, 86, 123, 59,
46, 53, 37, 101,
19, 86, 123, 59,
255, 255, 90, 20,
96, 42, 8, 72,
84, 90, 90, 20,
96, 42, 8, 72,
};
const uint8_t ntsc_chrominances[16] = {
255, 255, 121, 57,
@ -131,12 +121,12 @@ template <class BusHandler> class MOS6560 {
103, 42, 80, 16,
};
const uint8_t *chrominances;
Outputs::CRT::DisplayType display_type;
Outputs::Display::Type display_type;
switch(output_mode) {
default:
chrominances = pal_chrominances;
display_type = Outputs::CRT::DisplayType::PAL50;
display_type = Outputs::Display::Type::PAL50;
timing_.cycles_per_line = 71;
timing_.line_counter_increment_offset = 4;
timing_.final_line_increment_position = timing_.cycles_per_line - timing_.line_counter_increment_offset;
@ -146,7 +136,7 @@ template <class BusHandler> class MOS6560 {
case OutputMode::NTSC:
chrominances = ntsc_chrominances;
display_type = Outputs::CRT::DisplayType::NTSC60;
display_type = Outputs::Display::Type::NTSC60;
timing_.cycles_per_line = 65;
timing_.line_counter_increment_offset = 40;
timing_.final_line_increment_position = 58;
@ -155,14 +145,14 @@ template <class BusHandler> class MOS6560 {
break;
}
crt_->set_new_display_type(static_cast<unsigned int>(timing_.cycles_per_line*4), display_type);
crt_.set_new_display_type(timing_.cycles_per_line*4, display_type);
switch(output_mode) {
case OutputMode::PAL:
crt_->set_visible_area(Outputs::CRT::Rect(0.1f, 0.07f, 0.9f, 0.9f));
crt_.set_visible_area(Outputs::Display::Rect(0.1f, 0.07f, 0.9f, 0.9f));
break;
case OutputMode::NTSC:
crt_->set_visible_area(Outputs::CRT::Rect(0.05f, 0.05f, 0.9f, 0.9f));
crt_.set_visible_area(Outputs::Display::Rect(0.05f, 0.05f, 0.9f, 0.9f));
break;
}
@ -284,17 +274,17 @@ template <class BusHandler> class MOS6560 {
// update the CRT
if(this_state_ != output_state_) {
switch(output_state_) {
case State::Sync: crt_->output_sync(cycles_in_state_ * 4); break;
case State::ColourBurst: crt_->output_colour_burst(cycles_in_state_ * 4, (is_odd_frame_ || is_odd_line_) ? 128 : 0); break;
case State::Border: output_border(cycles_in_state_ * 4); break;
case State::Pixels: crt_->output_data(cycles_in_state_ * 4); break;
case State::Sync: crt_.output_sync(cycles_in_state_ * 4); break;
case State::ColourBurst: crt_.output_colour_burst(cycles_in_state_ * 4, (is_odd_frame_ || is_odd_line_) ? 128 : 0); break;
case State::Border: output_border(cycles_in_state_ * 4); break;
case State::Pixels: crt_.output_data(cycles_in_state_ * 4); break;
}
output_state_ = this_state_;
cycles_in_state_ = 0;
pixel_pointer = nullptr;
if(output_state_ == State::Pixels) {
pixel_pointer = reinterpret_cast<uint16_t *>(crt_->allocate_write_area(260));
pixel_pointer = reinterpret_cast<uint16_t *>(crt_.begin_data(260));
}
}
cycles_in_state_++;
@ -438,7 +428,7 @@ template <class BusHandler> class MOS6560 {
private:
BusHandler &bus_handler_;
std::unique_ptr<Outputs::CRT::CRT> crt_;
Outputs::CRT::CRT crt_;
Concurrency::DeferringAsyncTaskQueue audio_queue_;
AudioGenerator audio_generator_;
@ -465,7 +455,7 @@ template <class BusHandler> class MOS6560 {
enum State {
Sync, ColourBurst, Border, Pixels
} this_state_, output_state_;
unsigned int cycles_in_state_;
int cycles_in_state_;
// counters that cover an entire field
int horizontal_counter_ = 0, vertical_counter_ = 0;
@ -511,10 +501,10 @@ template <class BusHandler> class MOS6560 {
uint16_t colours_[16];
uint16_t *pixel_pointer;
void output_border(unsigned int number_of_cycles) {
uint16_t *colour_pointer = reinterpret_cast<uint16_t *>(crt_->allocate_write_area(1));
void output_border(int number_of_cycles) {
uint16_t *colour_pointer = reinterpret_cast<uint16_t *>(crt_.begin_data(1));
if(colour_pointer) *colour_pointer = registers_.borderColour;
crt_->output_level(number_of_cycles);
crt_.output_level(number_of_cycles);
}
struct {

View File

@ -50,7 +50,9 @@ struct ReverseTable {
Base::Base(Personality p) :
personality_(p),
crt_(new Outputs::CRT::CRT(CRTCyclesPerLine, CRTCyclesDivider, Outputs::CRT::DisplayType::NTSC60, 4)) {
crt_(CRTCyclesPerLine, CRTCyclesDivider, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Red8Green8Blue8) {
// Unimaginatively, this class just passes RGB through to the shader. Investigation is needed
// into whether there's a more natural form. It feels unlikely given the diversity of chips modelled.
switch(p) {
case TI::TMS::TMS9918A:
@ -83,22 +85,15 @@ Base::Base(Personality p) :
}
TMS9918::TMS9918(Personality p):
Base(p) {
// Unimaginatively, this class just passes RGB through to the shader. Investigation is needed
// into whether there's a more natural form.
crt_->set_rgb_sampling_function(
"vec3 rgb_sample(usampler2D sampler, vec2 coordinate)"
"{"
"return texture(sampler, coordinate).rgb / vec3(255.0);"
"}");
crt_->set_video_signal(Outputs::CRT::VideoSignal::RGB);
crt_->set_visible_area(Outputs::CRT::Rect(0.055f, 0.025f, 0.9f, 0.9f));
Base(p) {
crt_.set_display_type(Outputs::Display::DisplayType::RGB);
crt_.set_visible_area(Outputs::Display::Rect(0.07f, 0.0375f, 0.875f, 0.875f));
// The TMS remains in-phase with the NTSC colour clock; this is an empirical measurement
// intended to produce the correct relationship between the hard edges between pixels and
// the colour clock. It was eyeballed rather than derived from any knowledge of the TMS
// colour burst generator because I've yet to find any.
crt_->set_immediate_default_phase(0.85f);
crt_.set_immediate_default_phase(0.85f);
}
void TMS9918::set_tv_standard(TVStandard standard) {
@ -107,18 +102,22 @@ void TMS9918::set_tv_standard(TVStandard standard) {
case TVStandard::PAL:
mode_timing_.total_lines = 313;
mode_timing_.first_vsync_line = 253;
crt_->set_new_display_type(CRTCyclesPerLine, Outputs::CRT::DisplayType::PAL50);
crt_.set_new_display_type(CRTCyclesPerLine, Outputs::Display::Type::PAL50);
break;
default:
mode_timing_.total_lines = 262;
mode_timing_.first_vsync_line = 227;
crt_->set_new_display_type(CRTCyclesPerLine, Outputs::CRT::DisplayType::NTSC60);
crt_.set_new_display_type(CRTCyclesPerLine, Outputs::Display::Type::NTSC60);
break;
}
}
Outputs::CRT::CRT *TMS9918::get_crt() {
return crt_.get();
void TMS9918::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
crt_.set_scan_target(scan_target);
}
void TMS9918::set_display_type(Outputs::Display::DisplayType display_type) {
crt_.set_display_type(display_type);
}
void Base::LineBuffer::reset_sprite_collection() {
@ -367,7 +366,7 @@ void TMS9918::run_for(const HalfCycles cycles) {
if(read_pointer_.row >= mode_timing_.first_vsync_line && read_pointer_.row < mode_timing_.first_vsync_line+4) {
// Vertical sync.
if(end_column == 342) {
crt_->output_sync(342 * 4);
crt_.output_sync(342 * 4);
}
} else {
// Right border.
@ -377,11 +376,11 @@ void TMS9918::run_for(const HalfCycles cycles) {
// and 58+15 = 73. So output the lot when the
// cursor passes 73.
if(read_pointer_.column < 73 && end_column >= 73) {
crt_->output_blank(8*4);
crt_->output_sync(26*4);
crt_->output_blank(2*4);
crt_->output_default_colour_burst(14*4);
crt_->output_blank(8*4);
crt_.output_blank(8*4);
crt_.output_sync(26*4);
crt_.output_blank(2*4);
crt_.output_default_colour_burst(14*4);
crt_.output_blank(8*4);
}
// Border colour for the rest of the line.
@ -393,11 +392,11 @@ void TMS9918::run_for(const HalfCycles cycles) {
// Blanking region.
if(read_pointer_.column < 73 && end_column >= 73) {
crt_->output_blank(8*4);
crt_->output_sync(26*4);
crt_->output_blank(2*4);
crt_->output_default_colour_burst(14*4);
crt_->output_blank(8*4);
crt_.output_blank(8*4);
crt_.output_sync(26*4);
crt_.output_blank(2*4);
crt_.output_default_colour_burst(14*4);
crt_.output_blank(8*4);
}
// Left border.
@ -410,7 +409,7 @@ void TMS9918::run_for(const HalfCycles cycles) {
if(!asked_for_write_area_) {
asked_for_write_area_ = true;
pixel_origin_ = pixel_target_ = reinterpret_cast<uint32_t *>(
crt_->allocate_write_area(static_cast<unsigned int>(line_buffer.next_border_column - line_buffer.first_pixel_output_column))
crt_.begin_data(size_t(line_buffer.next_border_column - line_buffer.first_pixel_output_column))
);
}
@ -427,8 +426,8 @@ void TMS9918::run_for(const HalfCycles cycles) {
}
if(end == line_buffer.next_border_column) {
const unsigned int length = static_cast<unsigned int>(line_buffer.next_border_column - line_buffer.first_pixel_output_column);
crt_->output_data(length * 4, length);
const int length = line_buffer.next_border_column - line_buffer.first_pixel_output_column;
crt_.output_data(length * 4, size_t(length));
pixel_origin_ = pixel_target_ = nullptr;
asked_for_write_area_ = false;
}
@ -470,16 +469,26 @@ void Base::output_border(int cycles, uint32_t cram_dot) {
palette[background_colour_];
if(cram_dot) {
uint32_t *const pixel_target = reinterpret_cast<uint32_t *>(crt_->allocate_write_area(1));
*pixel_target = border_colour | cram_dot;
crt_->output_level(4);
uint32_t *const pixel_target = reinterpret_cast<uint32_t *>(crt_.begin_data(1));
if(pixel_target) {
*pixel_target = border_colour | cram_dot;
}
crt_.output_level(4);
cycles -= 4;
}
if(cycles) {
uint32_t *const pixel_target = reinterpret_cast<uint32_t *>(crt_->allocate_write_area(1));
*pixel_target = border_colour;
crt_->output_level(static_cast<unsigned int>(cycles));
// If the border colour is 0, that can be communicated
// more efficiently as an explicit blank.
if(border_colour) {
uint32_t *const pixel_target = reinterpret_cast<uint32_t *>(crt_.begin_data(1));
if(pixel_target) {
*pixel_target = border_colour;
}
crt_.output_level(cycles);
} else {
crt_.output_blank(cycles);
}
}
}

View File

@ -41,8 +41,11 @@ class TMS9918: public Base {
/*! Sets the TV standard for this TMS, if that is hard-coded in hardware. */
void set_tv_standard(TVStandard standard);
/*! Provides the CRT this TMS is connected to. */
Outputs::CRT::CRT *get_crt();
/*! Sets the scan target this TMS will post content to. */
void set_scan_target(Outputs::Display::ScanTarget *);
/*! Sets the type of display the CRT will request. */
void set_display_type(Outputs::Display::DisplayType);
/*!
Runs the VCP for the number of cycles indicate; it is an implicit assumption of the code

View File

@ -78,8 +78,8 @@ class Base {
Base(Personality p);
Personality personality_;
std::unique_ptr<Outputs::CRT::CRT> crt_;
const Personality personality_;
Outputs::CRT::CRT crt_;
TVStandard tv_standard_ = TVStandard::NTSC;
// Holds the contents of this VDP's connected DRAM.
@ -391,7 +391,7 @@ class Base {
/*
Fetching routines follow below; they obey the following rules:
1) input is a start position and an end position; they should perform the proper
1) input is a start position and an end position; they should perform the proper
operations for the period: start <= time < end.
2) times are measured relative to a 172-cycles-per-line clock (so: they directly
count access windows on the TMS and Master System).
@ -411,7 +411,7 @@ class Base {
Provided for the benefit of the methods below:
* the function external_slot(), which will perform any pending VRAM read/write.
* the function external_slot(), which will perform any pending VRAM read/write.
* the macros slot(n) and external_slot(n) which can be used to schedule those things inside a
switch(start)-based implementation.
@ -752,14 +752,14 @@ class Base {
fetch_tile_name(column+1, row_info) \
sprite_y_read(location+5, sprite); \
slot(location+6): \
slot(location+7): \
slot(location+7): \
slot(location+8): \
fetch_tile(column+1) \
fetch_tile_name(column+2, row_info) \
sprite_y_read(location+9, sprite+2); \
slot(location+10): \
slot(location+11): \
slot(location+12): \
slot(location+12): \
fetch_tile(column+2) \
fetch_tile_name(column+3, row_info) \
sprite_y_read(location+13, sprite+4); \

View File

@ -253,7 +253,7 @@ uint8_t AY38910::get_data_output() {
const uint8_t mask = port_handler_ ? port_handler_->get_port_input(selected_register_ == 15) : 0xff;
switch(selected_register_) {
default: break;
default: break;
case 14: return mask & ((registers_[0x7] & 0x40) ? registers_[14] : 0xff);
case 15: return mask & ((registers_[0x7] & 0x80) ? registers_[15] : 0xff);
}
@ -280,7 +280,7 @@ void AY38910::update_bus() {
// Assume no output, unless this turns out to be a read.
data_output_ = 0xff;
switch(control_state_) {
default: break;
default: break;
case LatchAddress: select_register(data_input_); break;
case Write: set_register_value(data_input_); break;
case Read: data_output_ = get_register_value(); break;

View File

@ -54,7 +54,7 @@ class DiskII:
void run_for(const Cycles cycles);
/*!
Supplies the image of the state machine (i.e. P6) ROM,
Supplies the image of the state machine (i.e. P6) ROM,
which dictates how the Disk II will respond to input.
To reduce processing costs, some assumptions are made by

View File

@ -33,11 +33,12 @@ bool get_bool(const Configurable::SelectionSet &selections_by_option, const std:
std::vector<std::unique_ptr<Configurable::Option>> Configurable::standard_options(Configurable::StandardOptions mask) {
std::vector<std::unique_ptr<Configurable::Option>> options;
if(mask & QuickLoadTape) options.emplace_back(new Configurable::BooleanOption("Load Tapes Quickly", "quickload"));
if(mask & (DisplayRGB | DisplayComposite | DisplaySVideo)) {
if(mask & (DisplayRGB | DisplayCompositeColour | DisplayCompositeMonochrome | DisplaySVideo)) {
std::vector<std::string> display_options;
if(mask & DisplayComposite) display_options.emplace_back("composite");
if(mask & DisplaySVideo) display_options.emplace_back("svideo");
if(mask & DisplayRGB) display_options.emplace_back("rgb");
if(mask & DisplayCompositeColour) display_options.emplace_back("composite");
if(mask & DisplayCompositeMonochrome) display_options.emplace_back("composite-mono");
if(mask & DisplaySVideo) display_options.emplace_back("svideo");
if(mask & DisplayRGB) display_options.emplace_back("rgb");
options.emplace_back(new Configurable::ListOption("Display", "display", display_options));
}
if(mask & AutomaticTapeMotorControl) options.emplace_back(new Configurable::BooleanOption("Automatic Tape Motor Control", "autotapemotor"));
@ -57,9 +58,10 @@ void Configurable::append_display_selection(Configurable::SelectionSet &selectio
std::string string_selection;
switch(selection) {
default:
case Display::RGB: string_selection = "rgb"; break;
case Display::SVideo: string_selection = "svideo"; break;
case Display::Composite: string_selection = "composite"; break;
case Display::RGB: string_selection = "rgb"; break;
case Display::SVideo: string_selection = "svideo"; break;
case Display::CompositeMonochrome: string_selection = "composite-mono"; break;
case Display::CompositeColour: string_selection = "composite"; break;
}
selection_set["display"] = std::unique_ptr<Configurable::Selection>(new Configurable::ListSelection(string_selection));
}
@ -85,7 +87,11 @@ bool Configurable::get_display(const Configurable::SelectionSet &selections_by_o
return true;
}
if(display->value == "composite") {
result = Configurable::Display::Composite;
result = Configurable::Display::CompositeColour;
return true;
}
if(display->value == "composite-mono") {
result = Configurable::Display::CompositeMonochrome;
return true;
}
}

View File

@ -16,15 +16,17 @@ namespace Configurable {
enum StandardOptions {
DisplayRGB = (1 << 0),
DisplaySVideo = (1 << 1),
DisplayComposite = (1 << 2),
QuickLoadTape = (1 << 3),
AutomaticTapeMotorControl = (1 << 4)
DisplayCompositeColour = (1 << 2),
DisplayCompositeMonochrome = (1 << 3),
QuickLoadTape = (1 << 4),
AutomaticTapeMotorControl = (1 << 5)
};
enum class Display {
RGB,
SVideo,
Composite
CompositeColour,
CompositeMonochrome
};
/*!

View File

@ -185,7 +185,7 @@ class ConcreteJoystick: public Joystick {
// convenient hard-coded values. TODO: make these a function of time.
using Type = Joystick::Input::Type;
switch(input.type) {
default: did_set_input(input, is_active ? 1.0f : 0.0f); break;
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.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;
@ -203,7 +203,7 @@ class ConcreteJoystick: public Joystick {
// Otherwise apply a threshold test to convert to digital, with remapping from axes to digital inputs.
using Type = Joystick::Input::Type;
switch(input.type) {
default: did_set_input(input, value > 0.5f); break;
default: did_set_input(input, value > 0.5f); break;
case Type::Horizontal:
did_set_input(Input(Type::Left, input.info.control.index), value <= 0.25f);
did_set_input(Input(Type::Right, input.info.control.index), value >= 0.75f);

View File

@ -30,6 +30,7 @@
#include "../../ClockReceiver/ForceInline.hpp"
#include "../../Outputs/Speaker/Implementation/LowpassSpeaker.hpp"
#include "../../Outputs/CRT/CRT.hpp"
#include "../../Analyser/Static/AmstradCPC/Target.hpp"
@ -40,7 +41,7 @@ namespace AmstradCPC {
std::vector<std::unique_ptr<Configurable::Option>> get_options() {
return Configurable::standard_options(
static_cast<Configurable::StandardOptions>(Configurable::DisplayRGB | Configurable::DisplayComposite)
static_cast<Configurable::StandardOptions>(Configurable::DisplayRGB | Configurable::DisplayCompositeColour)
);
}
@ -171,10 +172,14 @@ class AYDeferrer {
class CRTCBusHandler {
public:
CRTCBusHandler(uint8_t *ram, InterruptTimer &interrupt_timer) :
crt_(1024, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red2Green2Blue2),
ram_(ram),
interrupt_timer_(interrupt_timer) {
establish_palette_hits();
build_mode_table();
crt_.set_visible_area(Outputs::Display::Rect(0.1072f, 0.1f, 0.842105263157895f, 0.842105263157895f));
crt_.set_brightness(3.0f / 2.0f); // As only the values 0, 1 and 2 will be used in each channel,
// whereas Red2Green2Blue2 defines a range of 0-3.
}
/*!
@ -217,12 +222,12 @@ class CRTCBusHandler {
if(cycles_) {
switch(previous_output_mode_) {
default:
case OutputMode::Blank: crt_->output_blank(cycles_ * 16); break;
case OutputMode::Sync: crt_->output_sync(cycles_ * 16); break;
case OutputMode::Blank: crt_.output_blank(cycles_ * 16); break;
case OutputMode::Sync: crt_.output_sync(cycles_ * 16); break;
case OutputMode::Border: output_border(cycles_); break;
case OutputMode::ColourBurst: crt_->output_default_colour_burst(cycles_ * 16); break;
case OutputMode::ColourBurst: crt_.output_default_colour_burst(cycles_ * 16); break;
case OutputMode::Pixels:
crt_->output_data(cycles_ * 16, cycles_ * 16 / pixel_divider_);
crt_.output_data(cycles_ * 16, size_t(cycles_ * 16 / pixel_divider_));
pixel_pointer_ = pixel_data_ = nullptr;
break;
}
@ -238,7 +243,7 @@ class CRTCBusHandler {
// collect some more pixels if output is ongoing
if(previous_output_mode_ == OutputMode::Pixels) {
if(!pixel_data_) {
pixel_pointer_ = pixel_data_ = crt_->allocate_write_area(320, 8);
pixel_pointer_ = pixel_data_ = crt_.begin_data(320, 8);
}
if(pixel_pointer_) {
// the CPC shuffles output lines as:
@ -283,7 +288,7 @@ class CRTCBusHandler {
// widths so it's not necessarily possible to predict the correct number in advance
// and using the upper bound could lead to inefficient behaviour
if(pixel_pointer_ == pixel_data_ + 320) {
crt_->output_data(cycles_ * 16, cycles_ * 16 / pixel_divider_);
crt_.output_data(cycles_ * 16, size_t(cycles_ * 16 / pixel_divider_));
pixel_pointer_ = pixel_data_ = nullptr;
cycles_ = 0;
}
@ -323,27 +328,14 @@ class CRTCBusHandler {
was_hsync_ = state.hsync;
}
/// Constructs an appropriate CRT for video output.
void setup_output(float aspect_ratio) {
crt_.reset(new Outputs::CRT::CRT(1024, 16, Outputs::CRT::DisplayType::PAL50, 1));
crt_->set_rgb_sampling_function(
"vec3 rgb_sample(usampler2D sampler, vec2 coordinate)"
"{"
"uint sample = texture(texID, coordinate).r;"
"return vec3(float((sample >> 4) & 3u), float((sample >> 2) & 3u), float(sample & 3u)) / 2.0;"
"}");
crt_->set_visible_area(Outputs::CRT::Rect(0.1072f, 0.1f, 0.842105263157895f, 0.842105263157895f));
crt_->set_video_signal(Outputs::CRT::VideoSignal::RGB);
/// Sets the destination for output.
void set_scan_target(Outputs::Display::ScanTarget *scan_target) {
crt_.set_scan_target(scan_target);
}
/// Destructs the CRT.
void close_output() {
crt_.reset();
}
/// @returns the CRT.
Outputs::CRT::CRT *get_crt() {
return crt_.get();
/// Sets the type of display.
void set_display_type(Outputs::Display::DisplayType display_type) {
crt_.set_display_type(display_type);
}
/*!
@ -376,10 +368,10 @@ class CRTCBusHandler {
}
private:
void output_border(unsigned int length) {
uint8_t *colour_pointer = static_cast<uint8_t *>(crt_->allocate_write_area(1));
void output_border(int length) {
uint8_t *colour_pointer = static_cast<uint8_t *>(crt_.begin_data(1));
if(colour_pointer) *colour_pointer = border_;
crt_->output_level(length * 16);
crt_.output_level(length * 16);
}
#define Mode0Colour0(c) ((c & 0x80) >> 7) | ((c & 0x20) >> 3) | ((c & 0x08) >> 2) | ((c & 0x02) << 2)
@ -528,19 +520,19 @@ class CRTCBusHandler {
Border,
Pixels
} previous_output_mode_ = OutputMode::Sync;
unsigned int cycles_ = 0;
int cycles_ = 0;
bool was_hsync_ = false, was_vsync_ = false;
int cycles_into_hsync_ = 0;
std::unique_ptr<Outputs::CRT::CRT> crt_;
Outputs::CRT::CRT crt_;
uint8_t *pixel_data_ = nullptr, *pixel_pointer_ = nullptr;
uint8_t *ram_ = nullptr;
int next_mode_ = 2, mode_ = 2;
unsigned int pixel_divider_ = 1;
int pixel_divider_ = 1;
uint16_t mode0_output_[256];
uint32_t mode1_output_[256];
uint64_t mode2_output_[256];
@ -981,19 +973,14 @@ template <bool has_fdc> class ConcreteMachine:
flush_fdc();
}
/// A CRTMachine function; indicates that outputs should be created now.
void setup_output(float aspect_ratio) override final {
crtc_bus_handler_.setup_output(aspect_ratio);
/// A CRTMachine function; sets the destination for video.
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override final {
crtc_bus_handler_.set_scan_target(scan_target);
}
/// A CRTMachine function; indicates that outputs should be destroyed now.
void close_output() override final {
crtc_bus_handler_.close_output();
}
/// @returns the CRT in use.
Outputs::CRT::CRT *get_crt() override final {
return crtc_bus_handler_.get_crt();
/// A CRTMachine function; sets the output display type.
void set_display_type(Outputs::Display::DisplayType display_type) override final {
crtc_bus_handler_.set_display_type(display_type);
}
/// @returns the speaker in use.
@ -1192,7 +1179,7 @@ Machine *Machine::AmstradCPC(const Analyser::Static::Target *target, const ROMMa
using Target = Analyser::Static::AmstradCPC::Target;
const Target *const cpc_target = dynamic_cast<const Target *>(target);
switch(cpc_target->model) {
default: return new AmstradCPC::ConcreteMachine<true>(*cpc_target, rom_fetcher);
default: return new AmstradCPC::ConcreteMachine<true>(*cpc_target, rom_fetcher);
case Target::Model::CPC464: return new AmstradCPC::ConcreteMachine<false>(*cpc_target, rom_fetcher);
}
}

View File

@ -28,12 +28,19 @@
#include "../../Analyser/Static/AppleII/Target.hpp"
#include "../../ClockReceiver/ForceInline.hpp"
#include "../../Configurable/StandardOptions.hpp"
#include <algorithm>
#include <array>
#include <memory>
namespace {
namespace AppleII {
std::vector<std::unique_ptr<Configurable::Option>> get_options() {
return Configurable::standard_options(
static_cast<Configurable::StandardOptions>(Configurable::DisplayCompositeMonochrome | Configurable::DisplayCompositeColour)
);
}
#define is_iie() ((model == Analyser::Static::AppleII::Target::Model::IIe) || (model == Analyser::Static::AppleII::Target::Model::EnhancedIIe))
@ -43,6 +50,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
public KeyboardMachine::MappedMachine,
public CPU::MOS6502::BusHandler,
public Inputs::Keyboard,
public Configurable::Device,
public AppleII::Machine,
public Activity::Source,
public JoystickMachine::Machine,
@ -63,12 +71,12 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
CPU::MOS6502::Processor<(model == Analyser::Static::AppleII::Target::Model::EnhancedIIe) ? CPU::MOS6502::Personality::PSynertek65C02 : CPU::MOS6502::Personality::P6502, ConcreteMachine, false> m6502_;
VideoBusHandler video_bus_handler_;
std::unique_ptr<AppleII::Video::Video<VideoBusHandler, is_iie()>> video_;
AppleII::Video::Video<VideoBusHandler, is_iie()> video_;
int cycles_into_current_line_ = 0;
Cycles cycles_since_video_update_;
void update_video() {
video_->run_for(cycles_since_video_update_.flush());
video_.run_for(cycles_since_video_update_.flush());
}
static const int audio_divider = 8;
void update_audio() {
@ -84,7 +92,6 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
uint8_t ram_[65536], aux_ram_[65536];
std::vector<uint8_t> rom_;
std::vector<uint8_t> character_rom_;
uint8_t keyboard_input_ = 0x00;
bool key_is_down_ = false;
@ -123,7 +130,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
void pick_card_messaging_group(AppleII::Card *card) {
const bool is_every_cycle = is_every_cycle_card(card);
std::vector<AppleII::Card *> &intended = is_every_cycle ? every_cycle_cards_ : just_in_time_cards_;
std::vector<AppleII::Card *> &undesired = is_every_cycle ? just_in_time_cards_ : every_cycle_cards_;
std::vector<AppleII::Card *> &undesired = is_every_cycle ? just_in_time_cards_ : every_cycle_cards_;
if(std::find(intended.begin(), intended.end(), card) != intended.end()) return;
auto old_membership = std::find(undesired.begin(), undesired.end(), card);
@ -154,7 +161,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
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
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
@ -234,13 +241,13 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
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();
if(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()) {
if(video_.get_high_resolution()) {
page(0x20, 0x40,
use_aux_ram ? &aux_ram_[0x2000] : &ram_[0x2000],
use_aux_ram ? &aux_ram_[0x2000] : &ram_[0x2000]);
@ -308,15 +315,16 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
public:
ConcreteMachine(const Analyser::Static::AppleII::Target &target, const ROMMachine::ROMFetcher &rom_fetcher):
m6502_(*this),
video_bus_handler_(ram_, aux_ram_),
audio_toggle_(audio_queue_),
speaker_(audio_toggle_) {
// The system's master clock rate.
const float master_clock = 14318180.0;
video_bus_handler_(ram_, aux_ram_),
video_(video_bus_handler_),
audio_toggle_(audio_queue_),
speaker_(audio_toggle_) {
// The system's master clock rate.
const float master_clock = 14318180.0;
// This is where things get slightly convoluted: establish the machine as having a clock rate
// equal to the number of cycles of work the 6502 will actually achieve. Which is less than
// the master clock rate divided by 14 because every 65th cycle is extended by one seventh.
// This is where things get slightly convoluted: establish the machine as having a clock rate
// equal to the number of cycles of work the 6502 will actually achieve. Which is less than
// the master clock rate divided by 14 because every 65th cycle is extended by one seventh.
set_clock_rate((master_clock / 14.0) * 65.0 / (65.0 + 1.0 / 7.0));
// The speaker, however, should think it is clocked at half the master clock, per a general
@ -333,8 +341,8 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
Memory::Fuzz(aux_ram_, sizeof(aux_ram_));
// Add a couple of joysticks.
joysticks_.emplace_back(new Joystick);
joysticks_.emplace_back(new Joystick);
joysticks_.emplace_back(new Joystick);
joysticks_.emplace_back(new Joystick);
// Pick the required ROMs.
using Target = Analyser::Static::AppleII::Target;
@ -371,7 +379,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
rom_.erase(rom_.begin(), rom_.end() - static_cast<off_t>(rom_size));
}
character_rom_ = std::move(*roms[0]);
video_.set_character_rom(*roms[0]);
if(target.disk_controller != Target::DiskController::None) {
// Apple recommended slot 6 for the (first) Disk II.
@ -396,17 +404,13 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
audio_queue_.flush();
}
void setup_output(float aspect_ratio) override {
video_.reset(new AppleII::Video::Video<VideoBusHandler, is_iie()>(video_bus_handler_));
video_->set_character_rom(character_rom_);
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
video_.set_scan_target(scan_target);
}
void close_output() override {
video_.reset();
}
Outputs::CRT::CRT *get_crt() override {
return video_->get_crt();
/// Sets the type of display.
void set_display_type(Outputs::Display::DisplayType display_type) override {
video_.set_display_type(display_type);
}
Outputs::Speaker::Speaker *get_speaker() override {
@ -463,7 +467,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
// actor, but this will actually be the result most of the time so it's not
// too terrible.
if(isReadOperation(operation) && address != 0xc000) {
*value = video_->get_last_read_value(cycles_since_video_update_);
*value = video_.get_last_read_value(cycles_since_video_update_);
}
switch(address) {
@ -523,18 +527,18 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
case 0xc015: IIeSwitchRead(internal_CX_rom_); break;
case 0xc016: IIeSwitchRead(alternative_zero_page_); break;
case 0xc017: IIeSwitchRead(slot_C3_rom_); break;
case 0xc018: IIeSwitchRead(video_->get_80_store()); break;
case 0xc019: IIeSwitchRead(video_->get_is_vertical_blank(cycles_since_video_update_)); break;
case 0xc01a: IIeSwitchRead(video_->get_text()); break;
case 0xc01b: IIeSwitchRead(video_->get_mixed()); break;
case 0xc01c: IIeSwitchRead(video_->get_page2()); break;
case 0xc01d: IIeSwitchRead(video_->get_high_resolution()); break;
case 0xc01e: IIeSwitchRead(video_->get_alternative_character_set()); break;
case 0xc01f: IIeSwitchRead(video_->get_80_columns()); break;
case 0xc018: IIeSwitchRead(video_.get_80_store()); break;
case 0xc019: IIeSwitchRead(video_.get_is_vertical_blank(cycles_since_video_update_)); break;
case 0xc01a: IIeSwitchRead(video_.get_text()); break;
case 0xc01b: IIeSwitchRead(video_.get_mixed()); break;
case 0xc01c: IIeSwitchRead(video_.get_page2()); break;
case 0xc01d: IIeSwitchRead(video_.get_high_resolution()); break;
case 0xc01e: IIeSwitchRead(video_.get_alternative_character_set()); break;
case 0xc01f: IIeSwitchRead(video_.get_80_columns()); break;
#undef IIeSwitchRead
case 0xc07f:
if(is_iie()) *value = (*value & 0x7f) | (video_->get_annunciator_3() ? 0x80 : 0x00);
if(is_iie()) *value = (*value & 0x7f) | (video_.get_annunciator_3() ? 0x80 : 0x00);
break;
}
} else {
@ -546,7 +550,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
case 0xc000:
case 0xc001:
update_video();
video_->set_80_store(!!(address&1));
video_.set_80_store(!!(address&1));
set_main_paging();
break;
@ -586,13 +590,13 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
case 0xc00c:
case 0xc00d:
update_video();
video_->set_80_columns(!!(address&1));
video_.set_80_columns(!!(address&1));
break;
case 0xc00e:
case 0xc00f:
update_video();
video_->set_alternative_character_set(!!(address&1));
video_.set_alternative_character_set(!!(address&1));
break;
}
}
@ -615,20 +619,20 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
case 0xc050:
case 0xc051:
update_video();
video_->set_text(!!(address&1));
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 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));
video_.set_page2(!!(address&1));
set_main_paging();
break;
case 0xc056:
case 0xc057:
update_video();
video_->set_high_resolution(!!(address&1));
video_.set_high_resolution(!!(address&1));
set_main_paging();
break;
@ -636,7 +640,7 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
case 0xc05f:
if(is_iie()) {
update_video();
video_->set_annunciator_3(!(address&1));
video_.set_annunciator_3(!(address&1));
}
break;
@ -812,6 +816,28 @@ template <Analyser::Static::AppleII::Target::Model model> class ConcreteMachine:
string_serialiser_.reset(new Utility::StringSerialiser(string, true));
}
// MARK:: Configuration 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 {
Configurable::Display display;
if(Configurable::get_display(selections_by_option, display)) {
set_video_signal_configurable(display);
}
}
Configurable::SelectionSet get_accurate_selections() override {
Configurable::SelectionSet selection_set;
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
return selection_set;
}
Configurable::SelectionSet get_user_friendly_selections() override {
return get_accurate_selections();
}
// MARK: MediaTarget
bool insert_media(const Analyser::Static::Media &media) override {
if(!media.disks.empty()) {

View File

@ -18,6 +18,9 @@
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

@ -39,6 +39,7 @@ namespace AppleII {
*/
class Card {
public:
virtual ~Card() {}
enum Select: int {
None = 0, // No select line is active
IO = 1 << 0, // IO select is active

View File

@ -11,22 +11,19 @@
using namespace AppleII::Video;
VideoBase::VideoBase(bool is_iie, std::function<void(Cycles)> &&target) :
crt_(new Outputs::CRT::CRT(910, 1, Outputs::CRT::DisplayType::NTSC60, 1)),
crt_(910, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Luminance1),
is_iie_(is_iie),
deferrer_(std::move(target)) {
// Set a composite sampling function that assumes one byte per pixel input, and
// accepts any non-zero value as being fully on, zero being fully off.
crt_->set_composite_sampling_function(
"float composite_sample(usampler2D sampler, vec2 coordinate, float phase, float amplitude)"
"{"
"return clamp(texture(sampler, coordinate).r, 0.0, 0.66);"
"}");
// Show only the centre 75% of the TV frame.
crt_->set_video_signal(Outputs::CRT::VideoSignal::Composite);
crt_->set_visible_area(Outputs::CRT::Rect(0.118f, 0.122f, 0.77f, 0.77f));
crt_->set_immediate_default_phase(0.0f);
crt_.set_display_type(Outputs::Display::DisplayType::CompositeColour);
crt_.set_visible_area(Outputs::Display::Rect(0.118f, 0.122f, 0.77f, 0.77f));
// TODO: there seems to be some sort of bug whereby switching modes can cause
// a signal discontinuity that knocks phase out of whack. So it isn't safe to
// use default_colour_bursts elsewhere, though it otherwise should be. If/when
// it is, start doing so and return to setting the immediate phase up here.
// crt_.set_immediate_default_phase(0.5f);
character_zones[0].xor_mask = 0;
character_zones[0].address_mask = 0x3f;
@ -46,8 +43,12 @@ VideoBase::VideoBase(bool is_iie, std::function<void(Cycles)> &&target) :
}
}
Outputs::CRT::CRT *VideoBase::get_crt() {
return crt_.get();
void VideoBase::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
crt_.set_scan_target(scan_target);
}
void VideoBase::set_display_type(Outputs::Display::DisplayType display_type) {
crt_.set_display_type(display_type);
}
/*

View File

@ -36,8 +36,11 @@ class VideoBase {
public:
VideoBase(bool is_iie, std::function<void(Cycles)> &&target);
/// @returns The CRT this video feed is feeding.
Outputs::CRT::CRT *get_crt();
/// Sets the scan target.
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
/// Sets the type of output.
void set_display_type(Outputs::Display::DisplayType);
/*
Descriptions for the setters below are taken verbatim from
@ -146,7 +149,7 @@ class VideoBase {
void set_character_rom(const std::vector<uint8_t> &);
protected:
std::unique_ptr<Outputs::CRT::CRT> crt_;
Outputs::CRT::CRT crt_;
// State affecting output video stream generation.
uint8_t *pixel_pointer_ = nullptr;
@ -343,22 +346,23 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
while(int_cycles) {
const int cycles_this_line = std::min(65 - column_, int_cycles);
const int ending_column = column_ + cycles_this_line;
const bool is_vertical_sync_line = (row_ >= first_sync_line && row_ < first_sync_line + 3);
if(row_ >= first_sync_line && row_ < first_sync_line + 3) {
if(is_vertical_sync_line) {
// In effect apply an XOR to HSYNC and VSYNC flags in order to include equalising
// pulses (and hencce keep hsync approximately where it should be during vsync).
const int blank_start = std::max(first_sync_column - sync_length, column_);
const int blank_end = std::min(first_sync_column, ending_column);
if(blank_end > blank_start) {
if(blank_start > column_) {
crt_->output_sync(static_cast<unsigned int>(blank_start - column_) * 14);
crt_.output_sync((blank_start - column_) * 14);
}
crt_->output_blank(static_cast<unsigned int>(blank_end - blank_start) * 14);
crt_.output_blank((blank_end - blank_start) * 14);
if(blank_end < ending_column) {
crt_->output_sync(static_cast<unsigned int>(ending_column - blank_end) * 14);
crt_.output_sync((ending_column - blank_end) * 14);
}
} else {
crt_->output_sync(static_cast<unsigned int>(cycles_this_line) * 14);
crt_.output_sync(cycles_this_line * 14);
}
} else {
const GraphicsMode line_mode = graphics_mode(row_);
@ -394,7 +398,6 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
static_cast<size_t>(fetch_end - column_),
&base_stream_[static_cast<size_t>(column_)],
&auxiliary_stream_[static_cast<size_t>(column_)]);
// TODO: should character modes be mapped to character pixel outputs here?
}
if(row_ < 192) {
@ -402,7 +405,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
// remain where they would naturally be but auxiliary
// graphics appear to the left of that.
if(!column_) {
pixel_pointer_ = crt_->allocate_write_area(568);
pixel_pointer_ = crt_.begin_data(568);
graphics_carry_ = 0;
was_double_ = true;
}
@ -413,7 +416,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
const int pixel_row = row_ & 7;
const bool is_double = Video::is_double_mode(line_mode);
if(!is_double && was_double_) {
if(!is_double && was_double_ && pixel_pointer_) {
pixel_pointer_[pixel_start*14 + 0] =
pixel_pointer_[pixel_start*14 + 1] =
pixel_pointer_[pixel_start*14 + 2] =
@ -424,88 +427,92 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
}
was_double_ = is_double;
switch(line_mode) {
case GraphicsMode::Text:
output_text(
&pixel_pointer_[pixel_start * 14 + 7],
&base_stream_[static_cast<size_t>(pixel_start)],
static_cast<size_t>(pixel_end - pixel_start),
static_cast<size_t>(pixel_row));
break;
if(pixel_pointer_) {
switch(line_mode) {
case GraphicsMode::Text:
output_text(
&pixel_pointer_[pixel_start * 14 + 7],
&base_stream_[static_cast<size_t>(pixel_start)],
static_cast<size_t>(pixel_end - pixel_start),
static_cast<size_t>(pixel_row));
break;
case GraphicsMode::DoubleText:
output_double_text(
&pixel_pointer_[pixel_start * 14],
&base_stream_[static_cast<size_t>(pixel_start)],
&auxiliary_stream_[static_cast<size_t>(pixel_start)],
static_cast<size_t>(pixel_end - pixel_start),
static_cast<size_t>(pixel_row));
break;
case GraphicsMode::DoubleText:
output_double_text(
&pixel_pointer_[pixel_start * 14],
&base_stream_[static_cast<size_t>(pixel_start)],
&auxiliary_stream_[static_cast<size_t>(pixel_start)],
static_cast<size_t>(pixel_end - pixel_start),
static_cast<size_t>(pixel_row));
break;
case GraphicsMode::LowRes:
output_low_resolution(
&pixel_pointer_[pixel_start * 14 + 7],
&base_stream_[static_cast<size_t>(pixel_start)],
static_cast<size_t>(pixel_end - pixel_start),
pixel_start,
pixel_row);
break;
case GraphicsMode::LowRes:
output_low_resolution(
&pixel_pointer_[pixel_start * 14 + 7],
&base_stream_[static_cast<size_t>(pixel_start)],
static_cast<size_t>(pixel_end - pixel_start),
pixel_start,
pixel_row);
break;
case GraphicsMode::FatLowRes:
output_fat_low_resolution(
&pixel_pointer_[pixel_start * 14 + 7],
&base_stream_[static_cast<size_t>(pixel_start)],
static_cast<size_t>(pixel_end - pixel_start),
pixel_start,
pixel_row);
break;
case GraphicsMode::FatLowRes:
output_fat_low_resolution(
&pixel_pointer_[pixel_start * 14 + 7],
&base_stream_[static_cast<size_t>(pixel_start)],
static_cast<size_t>(pixel_end - pixel_start),
pixel_start,
pixel_row);
break;
case GraphicsMode::DoubleLowRes:
output_double_low_resolution(
&pixel_pointer_[pixel_start * 14],
&base_stream_[static_cast<size_t>(pixel_start)],
&auxiliary_stream_[static_cast<size_t>(pixel_start)],
static_cast<size_t>(pixel_end - pixel_start),
pixel_start,
pixel_row);
break;
case GraphicsMode::DoubleLowRes:
output_double_low_resolution(
&pixel_pointer_[pixel_start * 14],
&base_stream_[static_cast<size_t>(pixel_start)],
&auxiliary_stream_[static_cast<size_t>(pixel_start)],
static_cast<size_t>(pixel_end - pixel_start),
pixel_start,
pixel_row);
break;
case GraphicsMode::HighRes:
output_high_resolution(
&pixel_pointer_[pixel_start * 14 + 7],
&base_stream_[static_cast<size_t>(pixel_start)],
static_cast<size_t>(pixel_end - pixel_start));
break;
case GraphicsMode::HighRes:
output_high_resolution(
&pixel_pointer_[pixel_start * 14 + 7],
&base_stream_[static_cast<size_t>(pixel_start)],
static_cast<size_t>(pixel_end - pixel_start));
break;
case GraphicsMode::DoubleHighRes:
output_double_high_resolution(
&pixel_pointer_[pixel_start * 14],
&base_stream_[static_cast<size_t>(pixel_start)],
&auxiliary_stream_[static_cast<size_t>(pixel_start)],
static_cast<size_t>(pixel_end - pixel_start));
break;
case GraphicsMode::DoubleHighRes:
output_double_high_resolution(
&pixel_pointer_[pixel_start * 14],
&base_stream_[static_cast<size_t>(pixel_start)],
&auxiliary_stream_[static_cast<size_t>(pixel_start)],
static_cast<size_t>(pixel_end - pixel_start));
break;
default: break;
default: break;
}
}
if(pixel_end == 40) {
if(was_double_) {
pixel_pointer_[560] = pixel_pointer_[561] = pixel_pointer_[562] = pixel_pointer_[563] =
pixel_pointer_[564] = pixel_pointer_[565] = pixel_pointer_[566] = pixel_pointer_[567] = 0;
} else {
if(line_mode == GraphicsMode::HighRes && base_stream_[39]&0x80)
pixel_pointer_[567] = graphics_carry_;
else
pixel_pointer_[567] = 0;
if(pixel_pointer_) {
if(was_double_) {
pixel_pointer_[560] = pixel_pointer_[561] = pixel_pointer_[562] = pixel_pointer_[563] =
pixel_pointer_[564] = pixel_pointer_[565] = pixel_pointer_[566] = pixel_pointer_[567] = 0;
} else {
if(line_mode == GraphicsMode::HighRes && base_stream_[39]&0x80)
pixel_pointer_[567] = graphics_carry_;
else
pixel_pointer_[567] = 0;
}
}
crt_->output_data(568, 568);
crt_.output_data(568, 568);
pixel_pointer_ = nullptr;
}
}
} else {
if(column_ < 40 && ending_column >= 40) {
crt_->output_blank(568);
crt_.output_blank(568);
}
}
@ -515,11 +522,11 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
*/
if(column_ < first_sync_column && ending_column >= first_sync_column) {
crt_->output_blank((first_sync_column - 41)*14 - 1);
crt_.output_blank(first_sync_column*14 - 568);
}
if(column_ < (first_sync_column + sync_length) && ending_column >= (first_sync_column + sync_length)) {
crt_->output_sync(sync_length*14);
crt_.output_sync(sync_length*14);
}
int second_blank_start;
@ -527,7 +534,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
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_colour_burst(static_cast<unsigned int>(colour_burst_end - colour_burst_start) * 14, 192);
crt_.output_colour_burst((colour_burst_end - colour_burst_start) * 14, 0);
}
second_blank_start = std::max(first_sync_column + sync_length + 3, column_);
@ -536,7 +543,7 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
}
if(ending_column > second_blank_start) {
crt_->output_blank(static_cast<unsigned int>(ending_column - second_blank_start) * 14);
crt_.output_blank((ending_column - second_blank_start) * 14);
}
}
@ -550,8 +557,13 @@ template <class BusHandler, bool is_iie> class Video: public VideoBase {
}
// Add an extra half a colour cycle of blank; this isn't counted in the run_for
// count explicitly but is promised.
crt_->output_blank(2);
// count explicitly but is promised. If this is a vertical sync line, output sync
// instead of blank, taking that to be the default level.
if(is_vertical_sync_line) {
crt_.output_sync(2);
} else {
crt_.output_blank(2);
}
}
}
}

View File

@ -122,10 +122,6 @@ class ConcreteMachine:
joysticks_.emplace_back(new Joystick(bus_.get(), 4, 1));
}
~ConcreteMachine() {
close_output();
}
std::vector<std::unique_ptr<Inputs::Joystick>> &get_joysticks() override {
return joysticks_;
}
@ -157,18 +153,10 @@ class ConcreteMachine:
}
// to satisfy CRTMachine::Machine
void setup_output(float aspect_ratio) override {
bus_->tia_.reset(new TIA);
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
bus_->speaker_.set_input_rate(static_cast<float>(get_clock_rate() / static_cast<double>(CPUTicksPerAudioTick)));
bus_->tia_->get_crt()->set_delegate(this);
}
void close_output() override {
bus_.reset();
}
Outputs::CRT::CRT *get_crt() override {
return bus_->tia_->get_crt();
bus_->tia_.set_crt_delegate(this);
bus_->tia_.set_scan_target(scan_target);
}
Outputs::Speaker::Speaker *get_speaker() override {
@ -181,15 +169,15 @@ class ConcreteMachine:
}
// to satisfy Outputs::CRT::Delegate
void crt_did_end_batch_of_frames(Outputs::CRT::CRT *crt, unsigned int number_of_frames, unsigned int number_of_unexpected_vertical_syncs) override {
void crt_did_end_batch_of_frames(Outputs::CRT::CRT *crt, int number_of_frames, int number_of_unexpected_vertical_syncs) override {
const std::size_t number_of_frame_records = sizeof(frame_records_) / sizeof(frame_records_[0]);
frame_records_[frame_record_pointer_ % number_of_frame_records].number_of_frames = number_of_frames;
frame_records_[frame_record_pointer_ % number_of_frame_records].number_of_unexpected_vertical_syncs = number_of_unexpected_vertical_syncs;
frame_record_pointer_ ++;
if(frame_record_pointer_ >= 6) {
unsigned int total_number_of_frames = 0;
unsigned int total_number_of_unexpected_vertical_syncs = 0;
int total_number_of_frames = 0;
int total_number_of_unexpected_vertical_syncs = 0;
for(std::size_t c = 0; c < number_of_frame_records; c++) {
total_number_of_frames += frame_records_[c].number_of_frames;
total_number_of_unexpected_vertical_syncs += frame_records_[c].number_of_unexpected_vertical_syncs;
@ -205,10 +193,10 @@ class ConcreteMachine:
double clock_rate;
if(is_ntsc_) {
clock_rate = NTSC_clock_rate;
bus_->tia_->set_output_mode(TIA::OutputMode::NTSC);
bus_->tia_.set_output_mode(TIA::OutputMode::NTSC);
} else {
clock_rate = PAL_clock_rate;
bus_->tia_->set_output_mode(TIA::OutputMode::PAL);
bus_->tia_.set_output_mode(TIA::OutputMode::PAL);
}
bus_->speaker_.set_input_rate(static_cast<float>(clock_rate / static_cast<double>(CPUTicksPerAudioTick)));
@ -228,10 +216,8 @@ class ConcreteMachine:
// output frame rate tracker
struct FrameRecord {
unsigned int number_of_frames;
unsigned int number_of_unexpected_vertical_syncs;
FrameRecord() : number_of_frames(0), number_of_unexpected_vertical_syncs(0) {}
int number_of_frames = 0;
int number_of_unexpected_vertical_syncs = 0;
} frame_records_[4];
unsigned int frame_record_pointer_ = 0;
bool is_ntsc_ = true;

View File

@ -36,7 +36,7 @@ class Bus {
// the RIOT, TIA and speaker
PIA mos6532_;
std::shared_ptr<TIA> tia_;
TIA tia_;
Concurrency::DeferringAsyncTaskQueue audio_queue_;
TIASound tia_sound_;
@ -55,7 +55,7 @@ class Bus {
// video backlog accumulation counter
Cycles cycles_since_video_update_;
inline void update_video() {
tia_->run_for(cycles_since_video_update_.flush());
tia_.run_for(cycles_since_video_update_.flush());
}
// RIOT backlog accumulation counter

View File

@ -68,7 +68,7 @@ template<class T> class Cartridge:
// effect until the next read; therefore it isn't safe to assume that signalling ready immediately
// skips to the end of the line.
if(operation == CPU::MOS6502::BusOperation::Ready)
cycles_run_for = tia_->get_cycles_until_horizontal_blank(cycles_since_video_update_);
cycles_run_for = tia_.get_cycles_until_horizontal_blank(cycles_since_video_update_);
cycles_since_speaker_update_ += Cycles(cycles_run_for);
cycles_since_video_update_ += Cycles(cycles_run_for);
@ -101,7 +101,7 @@ template<class T> class Cartridge:
case 0x05: // missile 1 / playfield / ball collisions
case 0x06: // ball / playfield collisions
case 0x07: // player / player, missile / missile collisions
returnValue &= tia_->get_collision_flags(decodedAddress);
returnValue &= tia_.get_collision_flags(decodedAddress);
break;
case 0x08:
@ -120,52 +120,52 @@ template<class T> class Cartridge:
} else {
const uint16_t decodedAddress = address & 0x3f;
switch(decodedAddress) {
case 0x00: update_video(); tia_->set_sync(*value & 0x02); break;
case 0x01: update_video(); tia_->set_blank(*value & 0x02); break;
case 0x00: update_video(); tia_.set_sync(*value & 0x02); break;
case 0x01: update_video(); tia_.set_blank(*value & 0x02); break;
case 0x02: m6502_.set_ready_line(true); break;
case 0x03:
update_video();
tia_->reset_horizontal_counter();
tia_.reset_horizontal_counter();
horizontal_counter_resets_++;
break;
// TODO: audio will now be out of synchronisation. Fix.
case 0x04:
case 0x05: update_video(); tia_->set_player_number_and_size(decodedAddress - 0x04, *value); break;
case 0x05: update_video(); tia_.set_player_number_and_size(decodedAddress - 0x04, *value); break;
case 0x06:
case 0x07: update_video(); tia_->set_player_missile_colour(decodedAddress - 0x06, *value); break;
case 0x08: update_video(); tia_->set_playfield_ball_colour(*value); break;
case 0x09: update_video(); tia_->set_background_colour(*value); break;
case 0x0a: update_video(); tia_->set_playfield_control_and_ball_size(*value); break;
case 0x07: update_video(); tia_.set_player_missile_colour(decodedAddress - 0x06, *value); break;
case 0x08: update_video(); tia_.set_playfield_ball_colour(*value); break;
case 0x09: update_video(); tia_.set_background_colour(*value); break;
case 0x0a: update_video(); tia_.set_playfield_control_and_ball_size(*value); break;
case 0x0b:
case 0x0c: update_video(); tia_->set_player_reflected(decodedAddress - 0x0b, !((*value)&8)); break;
case 0x0c: update_video(); tia_.set_player_reflected(decodedAddress - 0x0b, !((*value)&8)); break;
case 0x0d:
case 0x0e:
case 0x0f: update_video(); tia_->set_playfield(decodedAddress - 0x0d, *value); break;
case 0x0f: update_video(); tia_.set_playfield(decodedAddress - 0x0d, *value); break;
case 0x10:
case 0x11: update_video(); tia_->set_player_position(decodedAddress - 0x10); break;
case 0x11: update_video(); tia_.set_player_position(decodedAddress - 0x10); break;
case 0x12:
case 0x13: update_video(); tia_->set_missile_position(decodedAddress - 0x12); break;
case 0x14: update_video(); tia_->set_ball_position(); break;
case 0x13: update_video(); tia_.set_missile_position(decodedAddress - 0x12); break;
case 0x14: update_video(); tia_.set_ball_position(); break;
case 0x1b:
case 0x1c: update_video(); tia_->set_player_graphic(decodedAddress - 0x1b, *value); break;
case 0x1c: update_video(); tia_.set_player_graphic(decodedAddress - 0x1b, *value); break;
case 0x1d:
case 0x1e: update_video(); tia_->set_missile_enable(decodedAddress - 0x1d, (*value)&2); break;
case 0x1f: update_video(); tia_->set_ball_enable((*value)&2); break;
case 0x1e: update_video(); tia_.set_missile_enable(decodedAddress - 0x1d, (*value)&2); break;
case 0x1f: update_video(); tia_.set_ball_enable((*value)&2); break;
case 0x20:
case 0x21: update_video(); tia_->set_player_motion(decodedAddress - 0x20, *value); break;
case 0x21: update_video(); tia_.set_player_motion(decodedAddress - 0x20, *value); break;
case 0x22:
case 0x23: update_video(); tia_->set_missile_motion(decodedAddress - 0x22, *value); break;
case 0x24: update_video(); tia_->set_ball_motion(*value); break;
case 0x23: update_video(); tia_.set_missile_motion(decodedAddress - 0x22, *value); break;
case 0x24: update_video(); tia_.set_ball_motion(*value); break;
case 0x25:
case 0x26: tia_->set_player_delay(decodedAddress - 0x25, (*value)&1); break;
case 0x27: tia_->set_ball_delay((*value)&1); break;
case 0x26: tia_.set_player_delay(decodedAddress - 0x25, (*value)&1); break;
case 0x27: tia_.set_ball_delay((*value)&1); break;
case 0x28:
case 0x29: update_video(); tia_->set_missile_position_to_player(decodedAddress - 0x28, (*value)&2); break;
case 0x2a: update_video(); tia_->move(); break;
case 0x2b: update_video(); tia_->clear_motion(); break;
case 0x2c: update_video(); tia_->clear_collision_flags(); break;
case 0x29: update_video(); tia_.set_missile_position_to_player(decodedAddress - 0x28, (*value)&2); break;
case 0x2a: update_video(); tia_.move(); break;
case 0x2b: update_video(); tia_.clear_motion(); break;
case 0x2c: update_video(); tia_.clear_collision_flags(); break;
case 0x15:
case 0x16: update_audio(); tia_sound_.set_control(decodedAddress - 0x15, *value); break;
@ -192,7 +192,7 @@ template<class T> class Cartridge:
}
}
if(!tia_->get_cycles_until_horizontal_blank(cycles_since_video_update_)) m6502_.set_ready_line(false);
if(!tia_.get_cycles_until_horizontal_blank(cycles_since_video_update_)) m6502_.set_ready_line(false);
return Cycles(cycles_run_for / 3);
}

View File

@ -22,12 +22,10 @@ namespace {
uint8_t reverse_table[256];
}
TIA::TIA(bool create_crt) {
if(create_crt) {
crt_.reset(new Outputs::CRT::CRT(cycles_per_line * 2 - 1, 1, Outputs::CRT::DisplayType::NTSC60, 1));
crt_->set_video_signal(Outputs::CRT::VideoSignal::Composite);
set_output_mode(OutputMode::NTSC);
}
TIA::TIA():
crt_(cycles_per_line * 2 - 1, 1, Outputs::Display::Type::NTSC60, Outputs::Display::InputDataType::Luminance8Phase8) {
set_output_mode(OutputMode::NTSC);
for(int c = 0; c < 256; c++) {
reverse_table[c] = static_cast<uint8_t>(
@ -113,51 +111,35 @@ TIA::TIA(bool create_crt) {
}
}
TIA::TIA() : TIA(true) {}
TIA::TIA(std::function<void(uint8_t *output_buffer)> line_end_function) : TIA(false) {
line_end_function_ = line_end_function;
}
void TIA::set_output_mode(Atari2600::TIA::OutputMode output_mode) {
Outputs::CRT::DisplayType display_type;
Outputs::Display::Type display_type;
tv_standard_ = output_mode;
if(output_mode == OutputMode::NTSC) {
crt_->set_svideo_sampling_function(
"vec2 svideo_sample(usampler2D texID, vec2 coordinate, float phase, float amplitude)"
"{"
"uint c = texture(texID, coordinate).r;"
"uint y = c & 14u;"
"uint iPhase = (c >> 4);"
"float phaseOffset = 6.283185308 * float(iPhase) / 13.0 + 5.074880441076923;"
"return vec2(float(y) / 14.0, step(1, iPhase) * cos(phase - phaseOffset));"
"}");
display_type = Outputs::CRT::DisplayType::NTSC60;
display_type = Outputs::Display::Type::NTSC60;
} else {
crt_->set_svideo_sampling_function(
"vec2 svideo_sample(usampler2D texID, vec2 coordinate, float phase, float amplitude)"
"{"
"uint c = texture(texID, coordinate).r;"
"uint y = c & 14u;"
"uint iPhase = (c >> 4);"
"uint direction = iPhase & 1u;"
"float phaseOffset = float(7u - direction) + (float(direction) - 0.5) * 2.0 * float(iPhase >> 1);"
"phaseOffset *= 6.283185308 / 12.0;"
"return vec2(float(y) / 14.0, step(4, (iPhase + 2u) & 15u) * cos(phase + phaseOffset));"
"}");
display_type = Outputs::CRT::DisplayType::PAL50;
display_type = Outputs::Display::Type::PAL50;
}
crt_->set_video_signal(Outputs::CRT::VideoSignal::Composite);
crt_.set_display_type(Outputs::Display::DisplayType::CompositeColour);
// line number of cycles in a line of video is one less than twice the number of clock cycles per line; the Atari
// outputs 228 colour cycles of material per line when an NTSC line 227.5. Since all clock numbers will be doubled
// later, cycles_per_line * 2 - 1 is therefore the real length of an NTSC line, even though we're going to supply
// cycles_per_line * 2 cycles of information from one sync edge to the next
crt_->set_new_display_type(cycles_per_line * 2 - 1, display_type);
crt_.set_new_display_type(cycles_per_line * 2 - 1, display_type);
/* speaker_->set_input_rate(static_cast<float>(get_clock_rate() / 38.0));*/
// Update the luminance/phase mappings of the current palette.
for(size_t c = 0; c < colour_palette_.size(); ++c) {
set_colour_palette_entry(c, colour_palette_[c].original);
}
}
void TIA::set_crt_delegate(Outputs::CRT::Delegate *delegate) {
crt_.set_delegate(delegate);
}
void TIA::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
crt_.set_scan_target(scan_target);
}
void TIA::run_for(const Cycles cycles) {
@ -198,7 +180,40 @@ int TIA::get_cycles_until_horizontal_blank(const Cycles from_offset) {
}
void TIA::set_background_colour(uint8_t colour) {
colour_palette_[static_cast<int>(ColourIndex::Background)] = colour;
set_colour_palette_entry(size_t(ColourIndex::Background), colour);
}
void TIA::set_colour_palette_entry(size_t index, uint8_t colour) {
const uint8_t luminance = ((colour & 14) * 255) / 14;
uint8_t phase = colour >> 4;
if(tv_standard_ == OutputMode::NTSC) {
if(!phase) phase = 255;
else {
phase = -(phase * 127) / 13;
phase -= 102;
phase &= 127;
}
} else {
if(phase < 2 || phase > 13) {
phase = 255;
} else {
const auto direction = phase & 1;
phase >>= 1;
if(direction) phase ^= 0xf;
phase = (phase + 6 + direction) & 0xf;
phase = (phase * 127) / 12;
phase &= 127;
}
}
colour_palette_[index].original = colour;
uint8_t *target = reinterpret_cast<uint8_t *>(&colour_palette_[index].luminance_phase);
target[0] = luminance;
target[1] = phase;
}
void TIA::set_playfield(uint16_t offset, uint8_t value) {
@ -238,7 +253,7 @@ void TIA::set_playfield_control_and_ball_size(uint8_t value) {
}
void TIA::set_playfield_ball_colour(uint8_t colour) {
colour_palette_[static_cast<int>(ColourIndex::PlayfieldBall)] = colour;
set_colour_palette_entry(size_t(ColourIndex::PlayfieldBall), colour);
}
void TIA::set_player_number_and_size(int player, uint8_t value) {
@ -300,7 +315,7 @@ void TIA::set_player_motion(int player, uint8_t motion) {
void TIA::set_player_missile_colour(int player, uint8_t colour) {
assert(player >= 0 && player < 2);
colour_palette_[static_cast<int>(ColourIndex::PlayerMissile0) + player] = colour;
set_colour_palette_entry(size_t(ColourIndex::PlayerMissile0) + size_t(player), colour);
}
void TIA::set_missile_enable(int missile, bool enabled) {
@ -382,7 +397,6 @@ void TIA::output_for_cycles(int number_of_cycles) {
bool is_reset = output_cursor < 224 && horizontal_counter_ >= 224;
if(!output_cursor) {
if(line_end_function_) line_end_function_(collision_buffer_);
std::memset(collision_buffer_, 0, sizeof(collision_buffer_));
ball_.motion_time %= 228;
@ -407,11 +421,11 @@ void TIA::output_for_cycles(int number_of_cycles) {
#define Period(function, target) \
if(output_cursor < target) { \
if(horizontal_counter_ <= target) { \
if(crt_) crt_->function(static_cast<unsigned int>((horizontal_counter_ - output_cursor) * 2)); \
crt_.function((horizontal_counter_ - output_cursor) * 2); \
horizontal_counter_ %= cycles_per_line; \
return; \
} else { \
if(crt_) crt_->function(static_cast<unsigned int>((target - output_cursor) * 2)); \
crt_.function((target - output_cursor) * 2); \
output_cursor = target; \
} \
}
@ -437,19 +451,17 @@ void TIA::output_for_cycles(int number_of_cycles) {
if(output_mode_ & blank_flag) {
if(pixel_target_) {
output_pixels(pixels_start_location_, output_cursor);
if(crt_) {
const unsigned int data_length = static_cast<unsigned int>(output_cursor - pixels_start_location_);
crt_->output_data(data_length * 2, data_length);
}
const int data_length = int(output_cursor - pixels_start_location_);
crt_.output_data(data_length * 2, size_t(data_length));
pixel_target_ = nullptr;
pixels_start_location_ = 0;
}
int duration = std::min(228, horizontal_counter_) - output_cursor;
if(crt_) crt_->output_blank(static_cast<unsigned int>(duration * 2));
crt_.output_blank(duration * 2);
} else {
if(!pixels_start_location_ && crt_) {
if(!pixels_start_location_) {
pixels_start_location_ = output_cursor;
pixel_target_ = crt_->allocate_write_area(160);
pixel_target_ = reinterpret_cast<uint16_t *>(crt_.begin_data(160));
}
// convert that into pixels
@ -461,9 +473,9 @@ void TIA::output_for_cycles(int number_of_cycles) {
output_cursor++;
}
if(horizontal_counter_ == cycles_per_line && crt_) {
const unsigned int data_length = static_cast<unsigned int>(output_cursor - pixels_start_location_);
crt_->output_data(data_length * 2, data_length);
if(horizontal_counter_ == cycles_per_line) {
const int data_length = int(output_cursor - pixels_start_location_);
crt_.output_data(data_length * 2, size_t(data_length));
pixel_target_ = nullptr;
pixels_start_location_ = 0;
}
@ -480,7 +492,7 @@ void TIA::output_pixels(int start, int end) {
if(start < first_pixel_cycle+8 && horizontal_blank_extend_) {
while(start < end && start < first_pixel_cycle+8) {
pixel_target_[target_position] = 0;
pixel_target_[target_position] = 0xff00; // TODO: this assumes little endianness.
start++;
target_position++;
}
@ -489,13 +501,13 @@ void TIA::output_pixels(int start, int end) {
if(playfield_priority_ == PlayfieldPriority::Score) {
while(start < end && start < first_pixel_cycle + 80) {
uint8_t buffer_value = collision_buffer_[start - first_pixel_cycle];
pixel_target_[target_position] = colour_palette_[colour_mask_by_mode_collision_flags_[static_cast<int>(ColourMode::ScoreLeft)][buffer_value]];
pixel_target_[target_position] = colour_palette_[colour_mask_by_mode_collision_flags_[static_cast<int>(ColourMode::ScoreLeft)][buffer_value]].luminance_phase;
start++;
target_position++;
}
while(start < end) {
uint8_t buffer_value = collision_buffer_[start - first_pixel_cycle];
pixel_target_[target_position] = colour_palette_[colour_mask_by_mode_collision_flags_[static_cast<int>(ColourMode::ScoreRight)][buffer_value]];
pixel_target_[target_position] = colour_palette_[colour_mask_by_mode_collision_flags_[static_cast<int>(ColourMode::ScoreRight)][buffer_value]].luminance_phase;
start++;
target_position++;
}
@ -503,7 +515,7 @@ void TIA::output_pixels(int start, int end) {
int table_index = static_cast<int>((playfield_priority_ == PlayfieldPriority::Standard) ? ColourMode::Standard : ColourMode::OnTop);
while(start < end) {
uint8_t buffer_value = collision_buffer_[start - first_pixel_cycle];
pixel_target_[target_position] = colour_palette_[colour_mask_by_mode_collision_flags_[table_index][buffer_value]];
pixel_target_[target_position] = colour_palette_[colour_mask_by_mode_collision_flags_[table_index][buffer_value]].luminance_phase;
start++;
target_position++;
}
@ -518,20 +530,16 @@ void TIA::output_line() {
break;
case sync_flag:
case sync_flag | blank_flag:
if(crt_) {
crt_->output_sync(32);
crt_->output_blank(32);
crt_->output_sync(392);
}
crt_.output_sync(32);
crt_.output_blank(32);
crt_.output_sync(392);
horizontal_blank_extend_ = false;
break;
case blank_flag:
if(crt_) {
crt_->output_blank(32);
crt_->output_sync(32);
crt_->output_default_colour_burst(32);
crt_->output_blank(360);
}
crt_.output_blank(32);
crt_.output_sync(32);
crt_.output_default_colour_burst(32);
crt_.output_blank(360);
horizontal_blank_extend_ = false;
break;
}

View File

@ -9,19 +9,19 @@
#ifndef TIA_hpp
#define TIA_hpp
#include <algorithm>
#include <array>
#include <cstdint>
#include <functional>
#include "../CRTMachine.hpp"
#include "../../Outputs/CRT/CRT.hpp"
#include "../../ClockReceiver/ClockReceiver.hpp"
namespace Atari2600 {
class TIA {
public:
TIA();
// The supplied hook is for unit testing only; if instantiated with a line_end_function then it will
// be called with the latest collision buffer upon the conclusion of each line. What's a collision
// buffer? It's an implementation detail. If you're not writing a unit test, leave it alone.
TIA(std::function<void(uint8_t *output_buffer)> line_end_function);
enum class OutputMode {
NTSC, PAL
@ -35,7 +35,7 @@ class TIA {
void set_sync(bool sync);
void set_blank(bool blank);
void reset_horizontal_counter(); // Reset is delayed by four cycles.
void reset_horizontal_counter(); // Reset is delayed by four cycles.
/*!
@returns the number of cycles between (current TIA time) + from_offset to the current or
@ -73,12 +73,11 @@ class TIA {
uint8_t get_collision_flags(int offset);
void clear_collision_flags();
Outputs::CRT::CRT *get_crt() { return crt_.get(); }
void set_crt_delegate(Outputs::CRT::Delegate *);
void set_scan_target(Outputs::Display::ScanTarget *);
private:
TIA(bool create_crt);
std::unique_ptr<Outputs::CRT::CRT> crt_;
std::function<void(uint8_t *output_buffer)> line_end_function_;
Outputs::CRT::CRT crt_;
// the master counter; counts from 0 to 228 with all visible pixels being in the final 160
int horizontal_counter_ = 0;
@ -107,7 +106,7 @@ class TIA {
ScoreRight,
OnTop
};
uint8_t colour_mask_by_mode_collision_flags_[4][64]; // maps from [ColourMode][CollisionMark] to colour_pallete_ entry
uint8_t colour_mask_by_mode_collision_flags_[4][64]; // maps from [ColourMode][CollisionMark] to colour_palette_ entry
enum class ColourIndex {
Background = 0,
@ -115,7 +114,13 @@ class TIA {
PlayerMissile0,
PlayerMissile1
};
uint8_t colour_palette_[4];
struct Colour {
uint16_t luminance_phase;
uint8_t original;
};
std::array<Colour, 4> colour_palette_;
void set_colour_palette_entry(size_t index, uint8_t colour);
OutputMode tv_standard_;
// playfield state
int background_half_mask_ = 0;
@ -302,7 +307,7 @@ class TIA {
inline void output_line();
int pixels_start_location_ = 0;
uint8_t *pixel_target_ = nullptr;
uint16_t *pixel_target_ = nullptr;
inline void output_pixels(int start, int end);
};

View File

@ -9,7 +9,7 @@
#ifndef CRTMachine_hpp
#define CRTMachine_hpp
#include "../Outputs/CRT/CRT.hpp"
#include "../Outputs/ScanTarget.hpp"
#include "../Outputs/Speaker/Speaker.hpp"
#include "../ClockReceiver/ClockReceiver.hpp"
#include "../ClockReceiver/TimeTypes.hpp"
@ -19,6 +19,7 @@
#include <cmath>
// TODO: rename.
namespace CRTMachine {
/*!
@ -29,19 +30,12 @@ namespace CRTMachine {
class Machine {
public:
/*!
Causes the machine to set up its CRT and, if it has one, speaker. The caller guarantees
that an OpenGL context is bound.
*/
virtual void setup_output(float aspect_ratio) = 0;
Causes the machine to set up its display and, if it has one, speaker.
/*!
Gives the machine a chance to release all owned resources. The caller guarantees that the
OpenGL context is bound.
The @c scan_target will receive all video output; the caller guarantees
that it is non-null.
*/
virtual void close_output() = 0;
/// @returns The CRT this machine is drawing to. Should not be @c nullptr.
virtual Outputs::CRT::CRT *get_crt() = 0;
virtual void set_scan_target(Outputs::Display::ScanTarget *scan_target) = 0;
/// @returns The speaker that receives this machine's output, or @c nullptr if this machine is mute.
virtual Outputs::Speaker::Speaker *get_speaker() = 0;
@ -68,32 +62,33 @@ class Machine {
}
/*!
Maps from Configurable::Display to Outputs::CRT::VideoSignal and calls
@c set_video_signal with the result.
Maps from Configurable::Display to Outputs::Display::VideoSignal and calls
@c set_display_type with the result.
*/
void set_video_signal_configurable(Configurable::Display type) {
Outputs::CRT::VideoSignal signal;
Outputs::Display::DisplayType display_type;
switch(type) {
default:
case Configurable::Display::RGB:
signal = Outputs::CRT::VideoSignal::RGB;
display_type = Outputs::Display::DisplayType::RGB;
break;
case Configurable::Display::SVideo:
signal = Outputs::CRT::VideoSignal::SVideo;
display_type = Outputs::Display::DisplayType::SVideo;
break;
case Configurable::Display::Composite:
signal = Outputs::CRT::VideoSignal::Composite;
case Configurable::Display::CompositeColour:
display_type = Outputs::Display::DisplayType::CompositeColour;
break;
case Configurable::Display::CompositeMonochrome:
display_type = Outputs::Display::DisplayType::CompositeMonochrome;
break;
}
set_video_signal(signal);
set_display_type(display_type);
}
/*!
Forwards the video signal to the CRT returned by get_crt().
Forwards the video signal to the target returned by get_crt().
*/
virtual void set_video_signal(Outputs::CRT::VideoSignal video_signal) {
get_crt()->set_video_signal(video_signal);
}
virtual void set_display_type(Outputs::Display::DisplayType display_type) {}
private:
double clock_rate_ = 1.0;

View File

@ -76,7 +76,7 @@ class Joystick: public Inputs::ConcreteJoystick {
}
break;
case Input::Up: if(is_active) direction_ &= ~0x01; else direction_ |= 0x01; break;
case Input::Up: if(is_active) direction_ &= ~0x01; else direction_ |= 0x01; break;
case Input::Right: if(is_active) direction_ &= ~0x02; else direction_ |= 0x02; break;
case Input::Down: if(is_active) direction_ &= ~0x04; else direction_ |= 0x04; break;
case Input::Left: if(is_active) direction_ &= ~0x08; else direction_ |= 0x08; break;
@ -112,6 +112,7 @@ class ConcreteMachine:
public:
ConcreteMachine(const Analyser::Static::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
z80_(*this),
vdp_(TI::TMS::TMS9918A),
sn76489_(TI::SN76489::Personality::SN76489, audio_queue_, sn76489_divider),
ay_(audio_queue_),
mixer_(sn76489_, ay_),
@ -159,6 +160,9 @@ class ConcreteMachine:
is_megacart_ = false;
}
}
// ColecoVisions have composite output only.
vdp_.set_display_type(Outputs::Display::DisplayType::CompositeColour);
}
~ConcreteMachine() {
@ -169,17 +173,12 @@ class ConcreteMachine:
return joysticks_;
}
void setup_output(float aspect_ratio) override {
vdp_.reset(new TI::TMS::TMS9918(TI::TMS::TMS9918A));
get_crt()->set_video_signal(Outputs::CRT::VideoSignal::Composite);
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
vdp_.set_scan_target(scan_target);
}
void close_output() override {
vdp_.reset();
}
Outputs::CRT::CRT *get_crt() override {
return vdp_->get_crt();
void set_display_type(Outputs::Display::DisplayType display_type) override {
vdp_.set_display_type(display_type);
}
Outputs::Speaker::Speaker *get_speaker() override {
@ -249,9 +248,9 @@ class ConcreteMachine:
switch((address >> 5) & 7) {
case 5:
update_video();
*cycle.value = vdp_->get_register(address);
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
*cycle.value = vdp_.get_register(address);
z80_.set_non_maskable_interrupt_line(vdp_.get_interrupt_line());
time_until_interrupt_ = vdp_.get_time_until_interrupt();
break;
case 7: {
@ -293,9 +292,9 @@ class ConcreteMachine:
case 5:
update_video();
vdp_->set_register(address, *cycle.value);
z80_.set_non_maskable_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
vdp_.set_register(address, *cycle.value);
z80_.set_non_maskable_interrupt_line(vdp_.get_interrupt_line());
time_until_interrupt_ = vdp_.get_time_until_interrupt();
break;
case 7:
@ -366,11 +365,11 @@ class ConcreteMachine:
speaker_.run_for(audio_queue_, time_since_sn76489_update_.divide_cycles(Cycles(sn76489_divider)));
}
inline void update_video() {
vdp_->run_for(time_since_vdp_update_.flush());
vdp_.run_for(time_since_vdp_update_.flush());
}
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
std::unique_ptr<TI::TMS::TMS9918> vdp_;
TI::TMS::TMS9918 vdp_;
Concurrency::DeferringAsyncTaskQueue audio_queue_;
TI::SN76489 sn76489_;

View File

@ -34,7 +34,7 @@ enum Key: uint16_t {
Key1 = key(0, 0x01), Key3 = key(0, 0x02), Key5 = key(0, 0x04), Key7 = key(0, 0x08),
Key9 = key(0, 0x10), KeyPlus = key(0, 0x20), KeyGBP = key(0, 0x40), KeyDelete = key(0, 0x80),
KeyRestore = 0xfffd
KeyRestore = 0xfffd
#undef key
};

View File

@ -50,7 +50,7 @@ enum ROMSlot {
std::vector<std::unique_ptr<Configurable::Option>> get_options() {
return Configurable::standard_options(
static_cast<Configurable::StandardOptions>(Configurable::DisplaySVideo | Configurable::DisplayComposite | Configurable::QuickLoadTape)
static_cast<Configurable::StandardOptions>(Configurable::DisplaySVideo | Configurable::DisplayCompositeColour | Configurable::QuickLoadTape)
);
}
@ -294,6 +294,7 @@ class ConcreteMachine:
public:
ConcreteMachine(const Analyser::Static::Commodore::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
m6502_(*this),
mos6560_(mos6560_bus_handler_),
user_port_via_port_handler_(new UserPortVIA),
keyboard_via_port_handler_(new KeyboardVIA),
serial_port_(new SerialPort),
@ -377,13 +378,16 @@ class ConcreteMachine:
if(target.region == Analyser::Static::Commodore::Target::Region::American || target.region == Analyser::Static::Commodore::Target::Region::Japanese) {
// NTSC
set_clock_rate(1022727);
output_mode_ = MOS::MOS6560::OutputMode::NTSC;
mos6560_.set_output_mode(MOS::MOS6560::OutputMode::NTSC);
} else {
// PAL
set_clock_rate(1108404);
output_mode_ = MOS::MOS6560::OutputMode::PAL;
mos6560_.set_output_mode(MOS::MOS6560::OutputMode::PAL);
}
mos6560_.set_high_frequency_cutoff(1600); // There is a 1.6Khz low-pass filter in the Vic-20.
mos6560_.set_clock_rate(get_clock_rate());
// Initialise the memory maps as all pointing to nothing
memset(processor_read_memory_map_, 0, sizeof(processor_read_memory_map_));
memset(processor_write_memory_map_, 0, sizeof(processor_write_memory_map_));
@ -501,7 +505,7 @@ class ConcreteMachine:
if((address&0xfc00) == 0x9000) {
if(!(address&0x100)) {
update_video();
result &= mos6560_->get_register(address);
result &= mos6560_.get_register(address);
}
if(address & 0x10) result &= user_port_via_.get_register(address);
if(address & 0x20) result &= keyboard_via_.get_register(address);
@ -588,7 +592,7 @@ class ConcreteMachine:
// The VIC is selected by bit 8 = 0
if(!(address&0x100)) {
update_video();
mos6560_->set_register(address, *value);
mos6560_.set_register(address, *value);
}
// The first VIA is selected by bit 4 = 1.
if(address & 0x10) user_port_via_.set_register(address, *value);
@ -613,30 +617,23 @@ class ConcreteMachine:
void flush() {
update_video();
mos6560_->flush();
mos6560_.flush();
}
void run_for(const Cycles cycles) override final {
m6502_.run_for(cycles);
}
void setup_output(float aspect_ratio) override final {
mos6560_.reset(new MOS::MOS6560::MOS6560<Vic6560BusHandler>(mos6560_bus_handler_));
mos6560_->set_high_frequency_cutoff(1600); // There is a 1.6Khz low-pass filter in the Vic-20.
mos6560_->set_output_mode(output_mode_);
mos6560_->set_clock_rate(get_clock_rate());
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override final {
mos6560_.set_scan_target(scan_target);
}
void close_output() override final {
mos6560_ = nullptr;
}
Outputs::CRT::CRT *get_crt() override final {
return mos6560_->get_crt();
void set_display_type(Outputs::Display::DisplayType display_type) override final {
mos6560_.set_display_type(display_type);
}
Outputs::Speaker::Speaker *get_speaker() override final {
return mos6560_->get_speaker();
return mos6560_.get_speaker();
}
void mos6522_did_change_interrupt_status(void *mos6522) override final {
@ -678,7 +675,7 @@ class ConcreteMachine:
Configurable::SelectionSet get_accurate_selections() override {
Configurable::SelectionSet selection_set;
Configurable::append_quick_load_tape_selection(selection_set, false);
Configurable::append_display_selection(selection_set, Configurable::Display::Composite);
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
return selection_set;
}
@ -701,7 +698,7 @@ class ConcreteMachine:
private:
void update_video() {
mos6560_->run_for(cycles_since_mos6560_update_.flush());
mos6560_.run_for(cycles_since_mos6560_update_.flush());
}
CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, ConcreteMachine, false> m6502_;
@ -731,8 +728,7 @@ class ConcreteMachine:
Cycles cycles_since_mos6560_update_;
Vic6560BusHandler mos6560_bus_handler_;
MOS::MOS6560::OutputMode output_mode_;
std::unique_ptr<MOS::MOS6560::MOS6560<Vic6560BusHandler>> mos6560_;
MOS::MOS6560::MOS6560<Vic6560BusHandler> mos6560_;
std::shared_ptr<UserPortVIA> user_port_via_port_handler_;
std::shared_ptr<KeyboardVIA> keyboard_via_port_handler_;
std::shared_ptr<SerialPort> serial_port_;

View File

@ -34,7 +34,7 @@ namespace Electron {
std::vector<std::unique_ptr<Configurable::Option>> get_options() {
return Configurable::standard_options(
static_cast<Configurable::StandardOptions>(Configurable::DisplayRGB | Configurable::DisplayComposite | Configurable::QuickLoadTape)
static_cast<Configurable::StandardOptions>(Configurable::DisplayRGB | Configurable::DisplayCompositeColour | Configurable::QuickLoadTape)
);
}
@ -51,6 +51,7 @@ class ConcreteMachine:
public:
ConcreteMachine(const Analyser::Static::Acorn::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
m6502_(*this),
video_output_(ram_),
sound_generator_(audio_queue_),
speaker_(sound_generator_) {
memset(key_states_, 0, sizeof(key_states_));
@ -160,7 +161,7 @@ class ConcreteMachine:
// for the entire frame, RAM is accessible only on odd cycles; in modes below 4
// it's also accessible only outside of the pixel regions
cycles += video_output_->get_cycles_until_next_ram_availability(cycles_since_display_update_.as_int() + 1);
cycles += video_output_.get_cycles_until_next_ram_availability(cycles_since_display_update_.as_int() + 1);
} else {
switch(address & 0xff0f) {
case 0xfe00:
@ -198,8 +199,8 @@ class ConcreteMachine:
case 0xfe0c: case 0xfe0d: case 0xfe0e: case 0xfe0f:
if(!isReadOperation(operation)) {
update_display();
video_output_->set_register(address, *value);
video_access_range_ = video_output_->get_memory_access_range();
video_output_.set_register(address, *value);
video_access_range_ = video_output_.get_memory_access_range();
queue_next_display_interrupt();
}
break;
@ -373,16 +374,12 @@ class ConcreteMachine:
audio_queue_.perform();
}
void setup_output(float aspect_ratio) override final {
video_output_.reset(new VideoOutput(ram_));
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override final {
video_output_.set_scan_target(scan_target);
}
void close_output() override final {
video_output_.reset();
}
Outputs::CRT::CRT *get_crt() override final {
return video_output_->get_crt();
void set_display_type(Outputs::Display::DisplayType display_type) override {
video_output_.set_display_type(display_type);
}
Outputs::Speaker::Speaker *get_speaker() override final {
@ -436,7 +433,7 @@ class ConcreteMachine:
Configurable::SelectionSet get_accurate_selections() override {
Configurable::SelectionSet selection_set;
Configurable::append_quick_load_tape_selection(selection_set, false);
Configurable::append_display_selection(selection_set, Configurable::Display::Composite);
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
return selection_set;
}
@ -508,12 +505,12 @@ class ConcreteMachine:
// MARK: - Work deferral updates.
inline void update_display() {
if(cycles_since_display_update_ > 0) {
video_output_->run_for(cycles_since_display_update_.flush());
video_output_.run_for(cycles_since_display_update_.flush());
}
}
inline void queue_next_display_interrupt() {
VideoOutput::Interrupt next_interrupt = video_output_->get_next_interrupt();
VideoOutput::Interrupt next_interrupt = video_output_.get_next_interrupt();
cycles_until_display_interrupt_ = next_interrupt.cycles;
next_display_interrupt_ = next_interrupt.interrupt;
}
@ -583,7 +580,7 @@ class ConcreteMachine:
int shift_restart_counter_ = 0;
// Outputs
std::unique_ptr<VideoOutput> video_output_;
VideoOutput video_output_;
Concurrency::DeferringAsyncTaskQueue audio_queue_;
SoundGenerator sound_generator_;

View File

@ -38,26 +38,26 @@ namespace {
// MARK: - Lifecycle
VideoOutput::VideoOutput(uint8_t *memory) : ram_(memory) {
VideoOutput::VideoOutput(uint8_t *memory) :
ram_(memory),
crt_(crt_cycles_per_line,
1,
Outputs::Display::Type::PAL50,
Outputs::Display::InputDataType::Red1Green1Blue1) {
memset(palette_, 0xf, sizeof(palette_));
setup_screen_map();
setup_base_address();
crt_.reset(new Outputs::CRT::CRT(crt_cycles_per_line, 8, Outputs::CRT::DisplayType::PAL50, 1));
crt_->set_rgb_sampling_function(
"vec3 rgb_sample(usampler2D sampler, vec2 coordinate)"
"{"
"uint texValue = texture(sampler, coordinate).r;"
"return vec3( uvec3(texValue) & uvec3(4u, 2u, 1u));"
"}");
// TODO: as implied below, I've introduced a clock's latency into the graphics pipeline somehow. Investigate.
crt_->set_visible_area(crt_->get_rect_for_area(first_graphics_line - 1, 256, (first_graphics_cycle+1) * crt_cycles_multiplier, 80 * crt_cycles_multiplier, 4.0f / 3.0f));
crt_.set_visible_area(crt_.get_rect_for_area(first_graphics_line - 1, 256, (first_graphics_cycle+1) * crt_cycles_multiplier, 80 * crt_cycles_multiplier, 4.0f / 3.0f));
}
// MARK: - CRT getter
void VideoOutput::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
crt_.set_scan_target(scan_target);
}
Outputs::CRT::CRT *VideoOutput::get_crt() {
return crt_.get();
void VideoOutput::set_display_type(Outputs::Display::DisplayType display_type) {
crt_.set_display_type(display_type);
}
// MARK: - Display update methods
@ -87,20 +87,20 @@ void VideoOutput::start_pixel_line() {
}
void VideoOutput::end_pixel_line() {
if(current_output_target_) {
const unsigned int data_length = static_cast<unsigned int>(current_output_target_ - initial_output_target_);
crt_->output_data(data_length * current_output_divider_, data_length);
const int data_length = int(current_output_target_ - initial_output_target_);
if(data_length) {
crt_.output_data(data_length * current_output_divider_, size_t(data_length));
}
current_character_row_++;
}
void VideoOutput::output_pixels(unsigned int number_of_cycles) {
void VideoOutput::output_pixels(int number_of_cycles) {
if(!number_of_cycles) return;
if(is_blank_line_) {
crt_->output_blank(number_of_cycles * crt_cycles_multiplier);
crt_.output_blank(number_of_cycles * crt_cycles_multiplier);
} else {
unsigned int divider = 1;
int divider = 1;
switch(screen_mode_) {
case 0: case 3: divider = 1; break;
case 1: case 4: case 6: divider = 2; break;
@ -108,12 +108,12 @@ void VideoOutput::output_pixels(unsigned int number_of_cycles) {
}
if(!initial_output_target_ || divider != current_output_divider_) {
if(current_output_target_) {
const unsigned int data_length = static_cast<unsigned int>(current_output_target_ - initial_output_target_);
crt_->output_data(data_length * current_output_divider_, data_length);
const int data_length = int(current_output_target_ - initial_output_target_);
if(data_length) {
crt_.output_data(data_length * current_output_divider_, size_t(data_length));
}
current_output_divider_ = divider;
initial_output_target_ = current_output_target_ = crt_->allocate_write_area(640 / current_output_divider_, 8 / divider);
initial_output_target_ = current_output_target_ = crt_.begin_data(size_t(640 / current_output_divider_), size_t(8 / divider));
}
#define get_pixel() \
@ -132,7 +132,7 @@ void VideoOutput::output_pixels(unsigned int number_of_cycles) {
current_output_target_ += 8;
current_pixel_column_++;
}
} else current_output_target_ += 4*number_of_cycles;
} else current_output_target_ += 8*number_of_cycles;
break;
case 1:
@ -143,7 +143,7 @@ void VideoOutput::output_pixels(unsigned int number_of_cycles) {
current_output_target_ += 4;
current_pixel_column_++;
}
} else current_output_target_ += 2*number_of_cycles;
} else current_output_target_ += 4*number_of_cycles;
break;
case 2:
@ -154,7 +154,7 @@ void VideoOutput::output_pixels(unsigned int number_of_cycles) {
current_output_target_ += 2;
current_pixel_column_++;
}
} else current_output_target_ += number_of_cycles;
} else current_output_target_ += 2*number_of_cycles;
break;
case 4: case 6:
@ -185,7 +185,7 @@ void VideoOutput::output_pixels(unsigned int number_of_cycles) {
current_output_target_ += 4;
current_pixel_column_++;
}
} else current_output_target_ += 2 * number_of_cycles;
} else current_output_target_ += 4 * number_of_cycles;
break;
case 5:
@ -216,7 +216,7 @@ void VideoOutput::output_pixels(unsigned int number_of_cycles) {
current_output_target_ += 2;
current_pixel_column_++;
}
} else current_output_target_ += number_of_cycles;
} else current_output_target_ += 2*number_of_cycles;
break;
}
@ -230,16 +230,16 @@ void VideoOutput::run_for(const Cycles cycles) {
while(number_of_cycles) {
int draw_action_length = screen_map_[screen_map_pointer_].length;
int time_left_in_action = std::min(number_of_cycles, draw_action_length - cycles_into_draw_action_);
if(screen_map_[screen_map_pointer_].type == DrawAction::Pixels) output_pixels(static_cast<unsigned int>(time_left_in_action));
if(screen_map_[screen_map_pointer_].type == DrawAction::Pixels) output_pixels(time_left_in_action);
number_of_cycles -= time_left_in_action;
cycles_into_draw_action_ += time_left_in_action;
if(cycles_into_draw_action_ == draw_action_length) {
switch(screen_map_[screen_map_pointer_].type) {
case DrawAction::Sync: crt_->output_sync(static_cast<unsigned int>(draw_action_length * crt_cycles_multiplier)); break;
case DrawAction::ColourBurst: crt_->output_default_colour_burst(static_cast<unsigned int>(draw_action_length * crt_cycles_multiplier)); break;
case DrawAction::Blank: crt_->output_blank(static_cast<unsigned int>(draw_action_length * crt_cycles_multiplier)); break;
case DrawAction::Pixels: end_pixel_line(); break;
case DrawAction::Sync: crt_.output_sync(draw_action_length * crt_cycles_multiplier); break;
case DrawAction::ColourBurst: crt_.output_default_colour_burst(draw_action_length * crt_cycles_multiplier); break;
case DrawAction::Blank: crt_.output_blank(draw_action_length * crt_cycles_multiplier); break;
case DrawAction::Pixels: end_pixel_line(); break;
}
screen_map_pointer_ = (screen_map_pointer_ + 1) % screen_map_.size();
cycles_into_draw_action_ = 0;

View File

@ -13,6 +13,8 @@
#include "../../ClockReceiver/ClockReceiver.hpp"
#include "Interrupts.hpp"
#include <vector>
namespace Electron {
/*!
@ -25,17 +27,21 @@ namespace Electron {
class VideoOutput {
public:
/*!
Instantiates a VideoOutput that will read its pixels from @c memory. The pointer supplied
should be to address 0 in the unexpanded Electron's memory map.
Instantiates a VideoOutput that will read its pixels from @c memory.
The pointer supplied should be to address 0 in the unexpanded Electron's memory map.
*/
VideoOutput(uint8_t *memory);
/// @returns the CRT to which output is being painted.
Outputs::CRT::CRT *get_crt();
/// Produces the next @c cycles of video output.
void run_for(const Cycles cycles);
/// Sets the destination for output.
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
/// Sets the type of output.
void set_display_type(Outputs::Display::DisplayType);
/*!
Writes @c value to the register at @c address. May mutate the results of @c get_next_interrupt,
@c get_cycles_until_next_ram_availability and @c get_memory_access_range.
@ -77,7 +83,7 @@ class VideoOutput {
private:
inline void start_pixel_line();
inline void end_pixel_line();
inline void output_pixels(unsigned int number_of_cycles);
inline void output_pixels(int number_of_cycles);
inline void setup_base_address();
int output_position_ = 0;
@ -109,9 +115,8 @@ class VideoOutput {
// CRT output
uint8_t *current_output_target_ = nullptr;
uint8_t *initial_output_target_ = nullptr;
unsigned int current_output_divider_ = 1;
std::unique_ptr<Outputs::CRT::CRT> crt_;
int current_output_divider_ = 1;
Outputs::CRT::CRT crt_;
struct DrawAction {
enum Type {

View File

@ -51,7 +51,7 @@ namespace MSX {
std::vector<std::unique_ptr<Configurable::Option>> get_options() {
return Configurable::standard_options(
static_cast<Configurable::StandardOptions>(Configurable::DisplayRGB | Configurable::DisplaySVideo | Configurable::DisplayComposite | Configurable::QuickLoadTape)
static_cast<Configurable::StandardOptions>(Configurable::DisplayRGB | Configurable::DisplaySVideo | Configurable::DisplayCompositeColour | Configurable::QuickLoadTape)
);
}
@ -148,6 +148,7 @@ class ConcreteMachine:
public:
ConcreteMachine(const Analyser::Static::MSX::Target &target, const ROMMachine::ROMFetcher &rom_fetcher):
z80_(*this),
vdp_(TI::TMS::TMS9918A),
i8255_(i8255_port_handler_),
ay_(audio_queue_),
audio_toggle_(audio_queue_),
@ -218,16 +219,12 @@ class ConcreteMachine:
audio_queue_.flush();
}
void setup_output(float aspect_ratio) override {
vdp_.reset(new TI::TMS::TMS9918(TI::TMS::TMS9918A));
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
vdp_.set_scan_target(scan_target);
}
void close_output() override {
vdp_.reset();
}
Outputs::CRT::CRT *get_crt() override {
return vdp_->get_crt();
void set_display_type(Outputs::Display::DisplayType display_type) override {
vdp_.set_display_type(display_type);
}
Outputs::Speaker::Speaker *get_speaker() override {
@ -451,10 +448,10 @@ class ConcreteMachine:
case CPU::Z80::PartialMachineCycle::Input:
switch(address & 0xff) {
case 0x98: case 0x99:
vdp_->run_for(time_since_vdp_update_.flush());
*cycle.value = vdp_->get_register(address);
z80_.set_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
vdp_.run_for(time_since_vdp_update_.flush());
*cycle.value = vdp_.get_register(address);
z80_.set_interrupt_line(vdp_.get_interrupt_line());
time_until_interrupt_ = vdp_.get_time_until_interrupt();
break;
case 0xa2:
@ -479,10 +476,10 @@ class ConcreteMachine:
const int port = address & 0xff;
switch(port) {
case 0x98: case 0x99:
vdp_->run_for(time_since_vdp_update_.flush());
vdp_->set_register(address, *cycle.value);
z80_.set_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
vdp_.run_for(time_since_vdp_update_.flush());
vdp_.set_register(address, *cycle.value);
z80_.set_interrupt_line(vdp_.get_interrupt_line());
time_until_interrupt_ = vdp_.get_time_until_interrupt();
break;
case 0xa0: case 0xa1:
@ -555,7 +552,7 @@ class ConcreteMachine:
}
void flush() {
vdp_->run_for(time_since_vdp_update_.flush());
vdp_.run_for(time_since_vdp_update_.flush());
update_audio();
audio_queue_.perform();
}
@ -603,7 +600,7 @@ class ConcreteMachine:
Configurable::SelectionSet get_accurate_selections() override {
Configurable::SelectionSet selection_set;
Configurable::append_quick_load_tape_selection(selection_set, false);
Configurable::append_display_selection(selection_set, Configurable::Display::Composite);
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
return selection_set;
}
@ -652,7 +649,7 @@ class ConcreteMachine:
case 2: {
// TODO:
// b6 caps lock LED
// b5 audio output
// b5 audio output
// b4: cassette motor relay
tape_player_.set_motor_control(!(value & 0x10));
@ -685,7 +682,7 @@ class ConcreteMachine:
};
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
std::unique_ptr<TI::TMS::TMS9918> vdp_;
TI::TMS::TMS9918 vdp_;
Intel::i8255::i8255<i8255PortHandler> i8255_;
Concurrency::DeferringAsyncTaskQueue audio_queue_;

View File

@ -43,6 +43,8 @@ class MemoryMap {
class ROMSlotHandler {
public:
virtual ~ROMSlotHandler() {}
/*! Advances time by @c half_cycles. */
virtual void run_for(HalfCycles half_cycles) {}

View File

@ -36,7 +36,7 @@ namespace MasterSystem {
std::vector<std::unique_ptr<Configurable::Option>> get_options() {
return Configurable::standard_options(
static_cast<Configurable::StandardOptions>(Configurable::DisplayRGB | Configurable::DisplayComposite)
static_cast<Configurable::StandardOptions>(Configurable::DisplayRGB | Configurable::DisplayCompositeColour)
);
}
@ -57,7 +57,7 @@ class Joystick: public Inputs::ConcreteJoystick {
switch(digital_input.type) {
default: return;
case Input::Up: if(is_active) state_ &= ~0x01; else state_ |= 0x01; break;
case Input::Up: if(is_active) state_ &= ~0x01; else state_ |= 0x01; break;
case Input::Down: if(is_active) state_ &= ~0x02; else state_ |= 0x02; break;
case Input::Left: if(is_active) state_ &= ~0x04; else state_ |= 0x04; break;
case Input::Right: if(is_active) state_ &= ~0x08; else state_ |= 0x08; break;
@ -94,6 +94,7 @@ class ConcreteMachine:
region_(target.region),
paging_scheme_(target.paging_scheme),
z80_(*this),
vdp_(tms_personality_for_model(target.model)),
sn76489_(
(target.model == Target::Model::SG1000) ? TI::SN76489::Personality::SN76489 : TI::SN76489::Personality::SMS,
audio_queue_,
@ -161,28 +162,17 @@ class ConcreteMachine:
audio_queue_.flush();
}
void setup_output(float aspect_ratio) override {
TI::TMS::Personality personality = TI::TMS::TMS9918A;
switch(model_) {
case Target::Model::SG1000: personality = TI::TMS::TMS9918A; break;
case Target::Model::MasterSystem: personality = TI::TMS::SMSVDP; break;
case Target::Model::MasterSystem2: personality = TI::TMS::SMS2VDP; break;
}
vdp_.reset(new TI::TMS::TMS9918(personality));
vdp_->set_tv_standard(
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override {
vdp_.set_tv_standard(
(region_ == Target::Region::Europe) ?
TI::TMS::TVStandard::PAL : TI::TMS::TVStandard::NTSC);
get_crt()->set_video_signal(Outputs::CRT::VideoSignal::Composite);
time_until_debounce_ = vdp_.get_time_until_line(-1);
time_until_debounce_ = vdp_->get_time_until_line(-1);
vdp_.set_scan_target(scan_target);
}
void close_output() override {
vdp_.reset();
}
Outputs::CRT::CRT *get_crt() override {
return vdp_->get_crt();
void set_display_type(Outputs::Display::DisplayType display_type) override {
vdp_.set_display_type(display_type);
}
Outputs::Speaker::Speaker *get_speaker() override {
@ -239,16 +229,16 @@ class ConcreteMachine:
break;
case 0x40:
update_video();
*cycle.value = vdp_->get_current_line();
*cycle.value = vdp_.get_current_line();
break;
case 0x41:
*cycle.value = vdp_->get_latched_horizontal_counter();
*cycle.value = vdp_.get_latched_horizontal_counter();
break;
case 0x80: case 0x81:
update_video();
*cycle.value = vdp_->get_register(address);
z80_.set_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
*cycle.value = vdp_.get_register(address);
z80_.set_interrupt_line(vdp_.get_interrupt_line());
time_until_interrupt_ = vdp_.get_time_until_interrupt();
break;
case 0xc0: {
Joystick *const joypad1 = static_cast<Joystick *>(joysticks_[0].get());
@ -290,7 +280,7 @@ class ConcreteMachine:
// Latch if either TH has newly gone to 1.
if((new_ths^previous_ths)&new_ths) {
update_video();
vdp_->latch_horizontal_counter();
vdp_.latch_horizontal_counter();
}
} break;
case 0x40: case 0x41:
@ -299,9 +289,9 @@ class ConcreteMachine:
break;
case 0x80: case 0x81:
update_video();
vdp_->set_register(address, *cycle.value);
z80_.set_interrupt_line(vdp_->get_interrupt_line());
time_until_interrupt_ = vdp_->get_time_until_interrupt();
vdp_.set_register(address, *cycle.value);
z80_.set_interrupt_line(vdp_.get_interrupt_line());
time_until_interrupt_ = vdp_.get_time_until_interrupt();
break;
case 0xc0:
LOG("TODO: [output] I/O port A/N; " << int(*cycle.value));
@ -337,7 +327,7 @@ class ConcreteMachine:
if(time_until_debounce_ <= HalfCycles(0)) {
z80_.set_non_maskable_interrupt_line(pause_is_pressed_);
update_video();
time_until_debounce_ = vdp_->get_time_until_line(-1);
time_until_debounce_ = vdp_.get_time_until_line(-1);
}
return HalfCycles(0);
@ -366,7 +356,6 @@ class ConcreteMachine:
}
}
void reset_all_keys(Inputs::Keyboard *) override {
}
@ -384,7 +373,7 @@ class ConcreteMachine:
Configurable::SelectionSet get_accurate_selections() override {
Configurable::SelectionSet selection_set;
Configurable::append_display_selection(selection_set, Configurable::Display::Composite);
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
return selection_set;
}
@ -395,6 +384,15 @@ class ConcreteMachine:
}
private:
static TI::TMS::Personality tms_personality_for_model(Analyser::Static::Sega::Target::Model model) {
switch(model) {
default:
case Target::Model::SG1000: return TI::TMS::TMS9918A;
case Target::Model::MasterSystem: return TI::TMS::SMSVDP;
case Target::Model::MasterSystem2: return TI::TMS::SMS2VDP;
}
}
inline uint8_t get_th_values() {
// Quick not on TH inputs here: if either is setup as an output, then the
// currently output level is returned. Otherwise they're fixed at 1.
@ -410,7 +408,7 @@ class ConcreteMachine:
speaker_.run_for(audio_queue_, time_since_sn76489_update_.divide_cycles(Cycles(sn76489_divider)));
}
inline void update_video() {
vdp_->run_for(time_since_vdp_update_.flush());
vdp_.run_for(time_since_vdp_update_.flush());
}
using Target = Analyser::Static::Sega::Target;
@ -418,7 +416,7 @@ class ConcreteMachine:
Target::Region region_;
Target::PagingScheme paging_scheme_;
CPU::Z80::Processor<ConcreteMachine, false, false> z80_;
std::unique_ptr<TI::TMS::TMS9918> vdp_;
TI::TMS::TMS9918 vdp_;
Concurrency::DeferringAsyncTaskQueue audio_queue_;
TI::SN76489 sn76489_;

View File

@ -29,10 +29,10 @@ uint16_t KeyboardMapper::mapped_key_for_key(Inputs::Keyboard::Key key) {
BIND(Hyphen, KeyMinus); BIND(Equals, KeyEquals); BIND(BackSlash, KeyBackSlash);
BIND(OpenSquareBracket, KeyOpenSquare); BIND(CloseSquareBracket, KeyCloseSquare);
BIND(BackSpace, KeyDelete); BIND(Delete, KeyDelete);
BIND(BackSpace, KeyDelete); BIND(Delete, KeyDelete);
BIND(Semicolon, KeySemiColon); BIND(Quote, KeyQuote);
BIND(Comma, KeyComma); BIND(FullStop, KeyFullStop); BIND(ForwardSlash, KeyForwardSlash);
BIND(Comma, KeyComma); BIND(FullStop, KeyFullStop); BIND(ForwardSlash, KeyForwardSlash);
BIND(Escape, KeyEscape); BIND(Tab, KeyEscape);
BIND(CapsLock, KeyControl); BIND(LeftControl, KeyControl); BIND(RightControl, KeyControl);

View File

@ -46,7 +46,11 @@ enum ROM {
std::vector<std::unique_ptr<Configurable::Option>> get_options() {
return Configurable::standard_options(
static_cast<Configurable::StandardOptions>(Configurable::DisplayRGB | Configurable::DisplayComposite | Configurable::QuickLoadTape)
static_cast<Configurable::StandardOptions>(
Configurable::DisplayRGB |
Configurable::DisplayCompositeColour |
Configurable::DisplayCompositeMonochrome |
Configurable::QuickLoadTape)
);
}
@ -208,12 +212,14 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
public:
ConcreteMachine(const Analyser::Static::Oric::Target &target, const ROMMachine::ROMFetcher &rom_fetcher) :
m6502_(*this),
video_output_(ram_),
ay8910_(audio_queue_),
speaker_(ay8910_),
via_port_handler_(audio_queue_, ay8910_, speaker_, tape_player_, keyboard_),
via_(via_port_handler_),
diskii_(2000000) {
set_clock_rate(1000000);
speaker_.set_input_rate(1000000.0f);
via_port_handler_.set_interrupt_delegate(this);
tape_player_.set_delegate(this);
Memory::Fuzz(ram_, sizeof(ram_));
@ -242,7 +248,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
}
}
colour_rom_ = std::move(*roms[0]);
video_output_.set_colour_rom(*roms[0]);
rom_ = std::move(*roms[1]);
switch(disk_interface) {
@ -263,7 +269,6 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
} break;
}
colour_rom_.resize(128);
rom_.resize(16384);
paged_rom_ = rom_.data();
@ -456,20 +461,12 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
}
// to satisfy CRTMachine::Machine
void setup_output(float aspect_ratio) override final {
speaker_.set_input_rate(1000000.0f);
video_output_.reset(new VideoOutput(ram_));
if(!colour_rom_.empty()) video_output_->set_colour_rom(colour_rom_);
set_video_signal(Outputs::CRT::VideoSignal::RGB);
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override final {
video_output_.set_scan_target(scan_target);
}
void close_output() override final {
video_output_.reset();
}
Outputs::CRT::CRT *get_crt() override final {
return video_output_->get_crt();
void set_display_type(Outputs::Display::DisplayType display_type) override {
video_output_.set_display_type(display_type);
}
Outputs::Speaker::Speaker *get_speaker() override final {
@ -537,14 +534,10 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
}
}
void set_video_signal(Outputs::CRT::VideoSignal video_signal) override {
video_output_->set_video_signal(video_signal);
}
Configurable::SelectionSet get_accurate_selections() override {
Configurable::SelectionSet selection_set;
Configurable::append_quick_load_tape_selection(selection_set, false);
Configurable::append_display_selection(selection_set, Configurable::Display::Composite);
Configurable::append_display_selection(selection_set, Configurable::Display::CompositeColour);
return selection_set;
}
@ -578,11 +571,11 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
CPU::MOS6502::Processor<CPU::MOS6502::Personality::P6502, ConcreteMachine, false> m6502_;
// RAM and ROM
std::vector<uint8_t> rom_, microdisc_rom_, colour_rom_;
std::vector<uint8_t> rom_, microdisc_rom_;
uint8_t ram_[65536];
Cycles cycles_since_video_update_;
inline void update_video() {
video_output_->run_for(cycles_since_video_update_.flush());
video_output_.run_for(cycles_since_video_update_.flush());
}
// ROM bookkeeping
@ -590,7 +583,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
int keyboard_read_count_ = 0;
// Outputs
std::unique_ptr<VideoOutput> video_output_;
VideoOutput video_output_;
Concurrency::DeferringAsyncTaskQueue audio_queue_;
GI::AY38910::AY38910 ay8910_;

View File

@ -8,6 +8,10 @@
#include "Video.hpp"
#include <algorithm>
//#define SUPPLY_COMPOSITE
using namespace Oric;
namespace {
@ -21,54 +25,66 @@ namespace {
VideoOutput::VideoOutput(uint8_t *memory) :
ram_(memory),
crt_(new Outputs::CRT::CRT(64*6, 6, Outputs::CRT::DisplayType::PAL50, 2)),
crt_(64*6, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Red1Green1Blue1),
v_sync_start_position_(PAL50VSyncStartPosition), v_sync_end_position_(PAL50VSyncEndPosition),
counter_period_(PAL50Period) {
crt_->set_rgb_sampling_function(
"vec3 rgb_sample(usampler2D sampler, vec2 coordinate)"
"{"
"uint texValue = texture(sampler, coordinate).r;"
"return vec3( uvec3(texValue) & uvec3(4u, 2u, 1u));"
"}");
crt_->set_composite_sampling_function(
"float composite_sample(usampler2D sampler, vec2 coordinate, float phase, float amplitude)"
"{"
"uint texValue = uint(dot(texture(sampler, coordinate).rg, uvec2(1, 256)));"
"uint iPhase = uint((phase + 3.141592654 + 0.39269908175) * 2.0 / 3.141592654) & 3u;"
"texValue = (texValue >> (4u*(3u - iPhase))) & 15u;"
"return (float(texValue) - 4.0) / 20.0;"
"}"
);
crt_->set_composite_function_type(Outputs::CRT::CRT::CompositeSourceType::DiscreteFourSamplesPerCycle, 0.0f);
set_video_signal(Outputs::CRT::VideoSignal::Composite);
crt_->set_visible_area(crt_->get_rect_for_area(54, 224, 16 * 6, 40 * 6, 4.0f / 3.0f));
crt_.set_visible_area(crt_.get_rect_for_area(54, 224, 16 * 6, 40 * 6, 4.0f / 3.0f));
crt_.set_phase_linked_luminance_offset(-1.0f / 8.0f);
data_type_ = Outputs::Display::InputDataType::Red1Green1Blue1;
crt_.set_input_data_type(data_type_);
}
void VideoOutput::set_video_signal(Outputs::CRT::VideoSignal video_signal) {
video_signal_ = video_signal;
crt_->set_video_signal(video_signal);
void VideoOutput::set_display_type(Outputs::Display::DisplayType display_type) {
crt_.set_display_type(display_type);
#ifdef SUPPLY_COMPOSITE
const auto data_type =
(display_type == Outputs::Display::DisplayType::RGB) ?
Outputs::Display::InputDataType::Red1Green1Blue1 :
Outputs::Display::InputDataType::PhaseLinkedLuminance8;
#else
const auto data_type = Outputs::Display::InputDataType::Red1Green1Blue1;
#endif
if(data_type_ != data_type) {
data_type_ = data_type;
crt_.set_input_data_type(data_type_);
}
}
void VideoOutput::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
crt_.set_scan_target(scan_target);
}
void VideoOutput::set_colour_rom(const std::vector<uint8_t> &rom) {
for(std::size_t c = 0; c < 8; c++) {
std::size_t index = (c << 2);
uint16_t rom_value = static_cast<uint16_t>((static_cast<uint16_t>(rom[index]) << 8) | static_cast<uint16_t>(rom[index+1]));
rom_value = (rom_value & 0xff00) | ((rom_value >> 4)&0x000f) | ((rom_value << 4)&0x00f0);
colour_forms_[c] = rom_value;
}
colour_forms_[c] = 0;
// check for big endianness and byte swap if required
uint16_t test_value = 0x0001;
if(*reinterpret_cast<uint8_t *>(&test_value) != 0x01) {
for(std::size_t c = 0; c < 8; c++) {
colour_forms_[c] = static_cast<uint16_t>((colour_forms_[c] >> 8) | (colour_forms_[c] << 8));
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);
colour[1] = uint8_t(rom[index] & 0xf0);
colour[2] = uint8_t(rom[index+1] & 0xf0);
colour[3] = uint8_t((rom[index+1] & 0x0f) << 4);
// Extracting just the visible part of the stored range of values
// means extracting the range 0x40 to 0xe0.
for(int sub = 0; sub < 4; ++sub) {
colour[sub] = ((colour[sub] - 0x40) * 255) / 0xa0;
}
}
}
Outputs::CRT::CRT *VideoOutput::get_crt() {
return crt_.get();
// Check for big endianness and byte swap if required.
// uint32_t test_value = 0x0001;
// if(*reinterpret_cast<uint8_t *>(&test_value) != 0x01) {
// for(std::size_t c = 0; c < 8; c++) {
// colour_forms_[c] = static_cast<uint16_t>((colour_forms_[c] >> 8) | (colour_forms_[c] << 8));
// }
// }
}
void VideoOutput::run_for(const Cycles cycles) {
@ -86,7 +102,7 @@ void VideoOutput::run_for(const Cycles cycles) {
if(counter_ >= v_sync_start_position_ && counter_ < v_sync_end_position_) {
// this is a sync line
cycles_run_for = v_sync_end_position_ - counter_;
clamp(crt_->output_sync(static_cast<unsigned int>(v_sync_end_position_ - v_sync_start_position_) * 6));
clamp(crt_.output_sync((v_sync_end_position_ - v_sync_start_position_) * 6));
} else if(counter_ < 224*64 && h_counter < 40) {
// this is a pixel line
if(!h_counter) {
@ -94,7 +110,12 @@ void VideoOutput::run_for(const Cycles cycles) {
paper_ = 0x0;
use_alternative_character_set_ = use_double_height_characters_ = blink_text_ = false;
set_character_set_base_address();
pixel_target_ = reinterpret_cast<uint16_t *>(crt_->allocate_write_area(240));
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));
}
if(!counter_) {
frame_counter_++;
@ -109,7 +130,7 @@ void VideoOutput::run_for(const Cycles cycles) {
int columns = cycles_run_for;
int pixel_base_address = 0xa000 + (counter_ >> 6) * 40;
int character_base_address = 0xbb80 + (counter_ >> 9) * 40;
uint8_t blink_mask = (blink_text_ && (frame_counter_&32)) ? 0x00 : 0xff;
const uint8_t blink_mask = (blink_text_ && (frame_counter_&32)) ? 0x00 : 0xff;
while(columns--) {
uint8_t pixels, control_byte;
@ -119,29 +140,36 @@ void VideoOutput::run_for(const Cycles cycles) {
} else {
int address = character_base_address + h_counter;
control_byte = ram_[address];
int line = use_double_height_characters_ ? ((counter_ >> 7) & 7) : ((counter_ >> 6) & 7);
const int line = use_double_height_characters_ ? ((counter_ >> 7) & 7) : ((counter_ >> 6) & 7);
pixels = ram_[character_set_base_address_ + (control_byte&127) * 8 + line];
}
uint8_t inverse_mask = (control_byte & 0x80) ? 0x7 : 0x0;
const uint8_t inverse_mask = (control_byte & 0x80) ? 0x7 : 0x0;
pixels &= blink_mask;
if(control_byte & 0x60) {
if(pixel_target_) {
uint16_t colours[2];
if(video_signal_ == Outputs::CRT::VideoSignal::RGB) {
colours[0] = static_cast<uint8_t>(paper_ ^ inverse_mask);
colours[1] = static_cast<uint8_t>(ink_ ^ inverse_mask);
} else {
colours[0] = colour_forms_[paper_ ^ inverse_mask];
colours[1] = colour_forms_[ink_ ^ inverse_mask];
}
pixel_target_[0] = colours[(pixels >> 5)&1];
pixel_target_[1] = colours[(pixels >> 4)&1];
pixel_target_[2] = colours[(pixels >> 3)&1];
pixel_target_[3] = colours[(pixels >> 2)&1];
pixel_target_[4] = colours[(pixels >> 1)&1];
pixel_target_[5] = colours[(pixels >> 0)&1];
if(data_type_ == Outputs::Display::InputDataType::Red1Green1Blue1 && rgb_pixel_target_) {
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];
} else if(composite_pixel_target_) {
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];
}
} else {
switch(control_byte & 0x1f) {
@ -179,19 +207,26 @@ void VideoOutput::run_for(const Cycles cycles) {
default: break;
}
if(pixel_target_) {
pixel_target_[0] = pixel_target_[1] =
pixel_target_[2] = pixel_target_[3] =
pixel_target_[4] = pixel_target_[5] =
(video_signal_ == Outputs::CRT::VideoSignal::RGB) ? paper_ ^ inverse_mask : colour_forms_[paper_ ^ inverse_mask];
if(data_type_ == Outputs::Display::InputDataType::Red1Green1Blue1 && rgb_pixel_target_) {
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;
} else if(composite_pixel_target_) {
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];
}
}
if(pixel_target_) pixel_target_ += 6;
if(rgb_pixel_target_) rgb_pixel_target_ += 6;
if(composite_pixel_target_) composite_pixel_target_ += 6;
h_counter++;
}
if(h_counter == 40) {
crt_->output_data(40 * 6);
crt_.output_data(40 * 6);
rgb_pixel_target_ = nullptr;
composite_pixel_target_ = nullptr;
}
} else {
// this is a blank line (or the equivalent part of a pixel line)
@ -199,17 +234,17 @@ void VideoOutput::run_for(const Cycles cycles) {
cycles_run_for = 48 - h_counter;
clamp(
int period = (counter_ < 224*64) ? 8 : 48;
crt_->output_blank(static_cast<unsigned int>(period) * 6);
crt_.output_blank(period * 6);
);
} else if(h_counter < 54) {
cycles_run_for = 54 - h_counter;
clamp(crt_->output_sync(6 * 6));
clamp(crt_.output_sync(6 * 6));
} else if(h_counter < 56) {
cycles_run_for = 56 - h_counter;
clamp(crt_->output_default_colour_burst(2 * 6));
clamp(crt_.output_default_colour_burst(2 * 6));
} else {
cycles_run_for = 64 - h_counter;
clamp(crt_->output_blank(8 * 6));
clamp(crt_.output_blank(8 * 6));
}
}

View File

@ -12,28 +12,35 @@
#include "../../Outputs/CRT/CRT.hpp"
#include "../../ClockReceiver/ClockReceiver.hpp"
#include <cstdint>
#include <memory>
#include <vector>
namespace Oric {
class VideoOutput {
public:
VideoOutput(uint8_t *memory);
Outputs::CRT::CRT *get_crt();
void set_colour_rom(const std::vector<uint8_t> &colour_rom);
void run_for(const Cycles cycles);
void set_colour_rom(const std::vector<uint8_t> &rom);
void set_video_signal(Outputs::CRT::VideoSignal output_device);
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
void set_display_type(Outputs::Display::DisplayType display_type);
private:
uint8_t *ram_;
std::unique_ptr<Outputs::CRT::CRT> crt_;
Outputs::CRT::CRT crt_;
// Counters and limits
int counter_ = 0, frame_counter_ = 0;
int v_sync_start_position_, v_sync_end_position_, counter_period_;
// Output target and device
uint16_t *pixel_target_;
uint16_t colour_forms_[8];
Outputs::CRT::VideoSignal video_signal_;
uint8_t *rgb_pixel_target_;
uint32_t *composite_pixel_target_;
uint32_t colour_forms_[8];
Outputs::Display::InputDataType data_type_;
// Registers
uint8_t ink_, paper_;

View File

@ -132,6 +132,7 @@ 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

@ -23,6 +23,8 @@ namespace Utility {
*/
class CharacterMapper {
public:
virtual ~CharacterMapper() {}
/// @returns The EndSequence-terminated sequence of keys that would cause @c character to be typed.
virtual uint16_t *sequence_for_character(char character) = 0;

View File

@ -8,6 +8,8 @@
#include "Video.hpp"
#include <algorithm>
using namespace ZX8081;
namespace {
@ -21,19 +23,11 @@ const std::size_t StandardAllocationSize = 320;
}
Video::Video() :
crt_(new Outputs::CRT::CRT(207 * 2, 1, Outputs::CRT::DisplayType::PAL50, 1)) {
// Set a composite sampling function that assumes two-level input; either a byte is 0, which is black,
// or it is non-zero, which is white.
crt_->set_composite_sampling_function(
"float composite_sample(usampler2D sampler, vec2 coordinate, float phase, float amplitude)"
"{"
"return texture(sampler, coordinate).r;"
"}");
crt_(207 * 2, 1, Outputs::Display::Type::PAL50, Outputs::Display::InputDataType::Luminance1) {
// Show only the centre 80% of the TV frame.
crt_->set_video_signal(Outputs::CRT::VideoSignal::Composite);
crt_->set_visible_area(Outputs::CRT::Rect(0.1f, 0.1f, 0.8f, 0.8f));
crt_.set_display_type(Outputs::Display::DisplayType::CompositeMonochrome);
crt_.set_visible_area(Outputs::Display::Rect(0.1f, 0.1f, 0.8f, 0.8f));
}
void Video::run_for(const HalfCycles half_cycles) {
@ -48,7 +42,7 @@ void Video::flush() {
void Video::flush(bool next_sync) {
if(sync_) {
// If in sync, that takes priority. Output the proper amount of sync.
crt_->output_sync(static_cast<unsigned int>(time_since_update_.as_int()));
crt_.output_sync(time_since_update_.as_int());
} else {
// If not presently in sync, then...
@ -58,16 +52,16 @@ void Video::flush(bool next_sync) {
int data_length = static_cast<int>(line_data_pointer_ - line_data_);
if(data_length < time_since_update_.as_int() || next_sync) {
auto output_length = std::min(data_length, time_since_update_.as_int());
crt_->output_data(static_cast<unsigned int>(output_length), static_cast<unsigned int>(output_length));
crt_.output_data(output_length);
line_data_pointer_ = line_data_ = nullptr;
time_since_update_ -= HalfCycles(output_length);
} else return;
}
// Any pending pixels being dealt with, pad with the white level.
uint8_t *colour_pointer = static_cast<uint8_t *>(crt_->allocate_write_area(1));
uint8_t *colour_pointer = static_cast<uint8_t *>(crt_.begin_data(1));
if(colour_pointer) *colour_pointer = 0xff;
crt_->output_level(static_cast<unsigned int>(time_since_update_.as_int()));
crt_.output_level(time_since_update_.as_int());
}
time_since_update_ = 0;
@ -89,16 +83,16 @@ void Video::output_byte(uint8_t byte) {
// Grab a buffer if one isn't already available.
if(!line_data_) {
line_data_pointer_ = line_data_ = crt_->allocate_write_area(StandardAllocationSize);
line_data_pointer_ = line_data_ = crt_.begin_data(StandardAllocationSize);
}
// If a buffer was obtained, serialise the new pixels.
if(line_data_) {
// If the buffer is full, output it now and obtain a new one
if(line_data_pointer_ - line_data_ == StandardAllocationSize) {
crt_->output_data(StandardAllocationSize, StandardAllocationSize);
crt_.output_data(StandardAllocationSize, StandardAllocationSize);
time_since_update_ -= StandardAllocationSize;
line_data_pointer_ = line_data_ = crt_->allocate_write_area(StandardAllocationSize);
line_data_pointer_ = line_data_ = crt_.begin_data(StandardAllocationSize);
if(!line_data_) return;
}
@ -112,6 +106,6 @@ void Video::output_byte(uint8_t byte) {
}
}
Outputs::CRT::CRT *Video::get_crt() {
return crt_.get();
void Video::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
crt_.set_scan_target(scan_target);
}

View File

@ -26,10 +26,8 @@ namespace ZX8081 {
*/
class Video {
public:
/// Constructs an instance of the video feed; a CRT is also created.
/// Constructs an instance of the video feed.
Video();
/// @returns The CRT this video feed is feeding.
Outputs::CRT::CRT *get_crt();
/// Advances time by @c half-cycles.
void run_for(const HalfCycles);
@ -41,12 +39,15 @@ class Video {
/// Causes @c byte to be serialised into pixels and output over the next four cycles.
void output_byte(uint8_t byte);
/// Sets the scan target.
void set_scan_target(Outputs::Display::ScanTarget *scan_target);
private:
bool sync_ = false;
uint8_t *line_data_ = nullptr;
uint8_t *line_data_pointer_ = nullptr;
HalfCycles time_since_update_ = 0;
std::unique_ptr<Outputs::CRT::CRT> crt_;
Outputs::CRT::CRT crt_;
void flush(bool next_sync);
};

View File

@ -135,23 +135,23 @@ template<bool is_zx81> class ConcreteMachine:
time_since_ay_update_ += cycle.length;
if(previous_counter < vsync_start_ && horizontal_counter_ >= vsync_start_) {
video_->run_for(vsync_start_ - previous_counter);
video_.run_for(vsync_start_ - previous_counter);
set_hsync(true);
line_counter_ = (line_counter_ + 1) & 7;
if(nmi_is_enabled_) {
z80_.set_non_maskable_interrupt_line(true);
}
video_->run_for(horizontal_counter_ - vsync_start_);
video_.run_for(horizontal_counter_ - vsync_start_);
} else if(previous_counter < vsync_end_ && horizontal_counter_ >= vsync_end_) {
video_->run_for(vsync_end_ - previous_counter);
video_.run_for(vsync_end_ - previous_counter);
set_hsync(false);
if(nmi_is_enabled_) {
z80_.set_non_maskable_interrupt_line(false);
z80_.set_wait_line(false);
}
video_->run_for(horizontal_counter_ - vsync_end_);
video_.run_for(horizontal_counter_ - vsync_end_);
} else {
video_->run_for(cycle.length);
video_.run_for(cycle.length);
}
if(is_zx81) horizontal_counter_ %= HalfCycles(Cycles(207));
@ -240,7 +240,7 @@ template<bool is_zx81> class ConcreteMachine:
latched_video_byte_ = ram_[address & ram_mask_] ^ mask;
}
video_->output_byte(latched_video_byte_);
video_.output_byte(latched_video_byte_);
has_latched_video_byte_ = false;
}
break;
@ -303,23 +303,15 @@ template<bool is_zx81> class ConcreteMachine:
}
forceinline void flush() {
video_->flush();
video_.flush();
if(is_zx81) {
update_audio();
audio_queue_.perform();
}
}
void setup_output(float aspect_ratio) override final {
video_.reset(new Video);
}
void close_output() override final {
video_.reset();
}
Outputs::CRT::CRT *get_crt() override final {
return video_->get_crt();
void set_scan_target(Outputs::Display::ScanTarget *scan_target) override final {
video_.set_scan_target(scan_target);
}
Outputs::Speaker::Speaker *get_speaker() override final {
@ -415,8 +407,7 @@ template<bool is_zx81> class ConcreteMachine:
private:
CPU::Z80::Processor<ConcreteMachine, false, is_zx81> z80_;
std::unique_ptr<Video> video_;
Video video_;
uint16_t tape_trap_address_, tape_return_address_;
uint16_t automatic_tape_motor_start_address_, automatic_tape_motor_end_address_;
@ -464,7 +455,7 @@ template<bool is_zx81> class ConcreteMachine:
}
inline void update_sync() {
video_->set_sync(vsync_ || hsync_);
video_.set_sync(vsync_ || hsync_);
}
// MARK: - Audio

View File

@ -12,6 +12,8 @@
4B0333AF2094081A0050B93D /* AppleDSK.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0333AD2094081A0050B93D /* AppleDSK.cpp */; };
4B0333B02094081A0050B93D /* AppleDSK.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0333AD2094081A0050B93D /* AppleDSK.cpp */; };
4B049CDD1DA3C82F00322067 /* BCDTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B049CDC1DA3C82F00322067 /* BCDTest.swift */; };
4B05401E219D1618001BF69C /* ScanTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B05401D219D1618001BF69C /* ScanTarget.cpp */; };
4B05401F219D1618001BF69C /* ScanTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B05401D219D1618001BF69C /* ScanTarget.cpp */; };
4B055A7A1FAE78A00060FFFF /* SDL2.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4B055A771FAE78210060FFFF /* SDL2.framework */; };
4B055A7E1FAE84AA0060FFFF /* main.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B055A7C1FAE84A50060FFFF /* main.cpp */; };
4B055A8D1FAE85920060FFFF /* AsyncTaskQueue.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B3940E51DA83C8300427841 /* AsyncTaskQueue.cpp */; };
@ -94,13 +96,6 @@
4B055ADE1FAE9B4C0060FFFF /* 6522Base.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B83348B1F5DB99C0097E338 /* 6522Base.cpp */; };
4B055ADF1FAE9B4C0060FFFF /* IRQDelegatePortHandler.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8334891F5DB94B0097E338 /* IRQDelegatePortHandler.cpp */; };
4B055AE01FAE9B660060FFFF /* CRT.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B0CCC421C62D0B3001CAC5F /* CRT.cpp */; };
4B055AE11FAE9B6F0060FFFF /* ArrayBuilder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B5073051DDD3B9400C48FBD /* ArrayBuilder.cpp */; };
4B055AE21FAE9B6F0060FFFF /* CRTOpenGL.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF990A1C8FBA6F0075DAFB /* CRTOpenGL.cpp */; };
4B055AE31FAE9B6F0060FFFF /* TextureBuilder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF99081C8FBA6F0075DAFB /* TextureBuilder.cpp */; };
4B055AE41FAE9B6F0060FFFF /* TextureTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF99121C8FBA6F0075DAFB /* TextureTarget.cpp */; };
4B055AE51FAE9B6F0060FFFF /* IntermediateShader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBB142F1CD2CECE00BDB55C /* IntermediateShader.cpp */; };
4B055AE61FAE9B6F0060FFFF /* OutputShader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC3B7501CD1956900F86E85 /* OutputShader.cpp */; };
4B055AE71FAE9B6F0060FFFF /* Shader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC3B74D1CD194CC00F86E85 /* Shader.cpp */; };
4B055AE81FAE9B7B0060FFFF /* FIRFilter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC76E671C98E31700E6EF73 /* FIRFilter.cpp */; };
4B055AE91FAE9B990060FFFF /* 6502Base.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B6A4C951F58F09E00E3F787 /* 6502Base.cpp */; };
4B055AEA1FAE9B990060FFFF /* 6502Storage.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B8334851F5DA3780097E338 /* 6502Storage.cpp */; };
@ -203,7 +198,6 @@
4B4B1A3D200198CA00A0F866 /* KonamiSCC.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B4B1A3A200198C900A0F866 /* KonamiSCC.cpp */; };
4B4DC8211D2C2425003C5BF8 /* Vic20.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B4DC81F1D2C2425003C5BF8 /* Vic20.cpp */; };
4B4DC82B1D2C27A4003C5BF8 /* SerialBus.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B4DC8291D2C27A4003C5BF8 /* SerialBus.cpp */; };
4B5073071DDD3B9400C48FBD /* ArrayBuilder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B5073051DDD3B9400C48FBD /* ArrayBuilder.cpp */; };
4B50730A1DDFCFDF00C48FBD /* ArrayBuilderTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B5073091DDFCFDF00C48FBD /* ArrayBuilderTests.mm */; };
4B54C0BC1F8D8E790050900F /* KeyboardMachine.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C0BB1F8D8E790050900F /* KeyboardMachine.cpp */; };
4B54C0BF1F8D8F450050900F /* Keyboard.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B54C0BD1F8D8F450050900F /* Keyboard.cpp */; };
@ -600,38 +594,40 @@
4BB73EAC1B587A5100552FC2 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4BB73EAA1B587A5100552FC2 /* MainMenu.xib */; };
4BB73EB71B587A5100552FC2 /* AllSuiteATests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB73EB61B587A5100552FC2 /* AllSuiteATests.swift */; };
4BB73EC21B587A5100552FC2 /* Clock_SignalUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB73EC11B587A5100552FC2 /* Clock_SignalUITests.swift */; };
4BBB14311CD2CECE00BDB55C /* IntermediateShader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBB142F1CD2CECE00BDB55C /* IntermediateShader.cpp */; };
4BBB70A4202011C2002FE009 /* MultiMediaTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBB70A3202011C2002FE009 /* MultiMediaTarget.cpp */; };
4BBB70A5202011C2002FE009 /* MultiMediaTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBB70A3202011C2002FE009 /* MultiMediaTarget.cpp */; };
4BBB70A8202014E2002FE009 /* MultiCRTMachine.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBB70A6202014E2002FE009 /* MultiCRTMachine.cpp */; };
4BBB70A9202014E2002FE009 /* MultiCRTMachine.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBB70A6202014E2002FE009 /* MultiCRTMachine.cpp */; };
4BBC951E1F368D83008F4C34 /* i8272.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBC951C1F368D83008F4C34 /* i8272.cpp */; };
4BBF49AF1ED2880200AB3669 /* FUSETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF49AE1ED2880200AB3669 /* FUSETests.swift */; };
4BBF99141C8FBA6F0075DAFB /* TextureBuilder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BBF99081C8FBA6F0075DAFB /* TextureBuilder.cpp */; };
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 */; };
4BC3B74F1CD194CC00F86E85 /* Shader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC3B74D1CD194CC00F86E85 /* Shader.cpp */; };
4BC3B7521CD1956900F86E85 /* OutputShader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC3B7501CD1956900F86E85 /* OutputShader.cpp */; };
4BC5FC3020CDDDEF00410AA0 /* AppleIIOptions.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4BC5FC2E20CDDDEE00410AA0 /* AppleIIOptions.xib */; };
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 */; };
4BC9E1EE1D23449A003FCEE4 /* 6502InterruptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC9E1ED1D23449A003FCEE4 /* 6502InterruptTests.swift */; };
4BCA6CC81D9DD9F000C2D7B2 /* CommodoreROM.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCA6CC61D9DD9F000C2D7B2 /* CommodoreROM.cpp */; };
4BCF1FA41DADC3DD0039D2E7 /* Oric.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCF1FA21DADC3DD0039D2E7 /* Oric.cpp */; };
4BD191F42191180E0042E144 /* ScanTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD191F22191180E0042E144 /* ScanTarget.cpp */; };
4BD191F52191180E0042E144 /* ScanTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD191F22191180E0042E144 /* ScanTarget.cpp */; };
4BD3A30B1EE755C800B5B501 /* Video.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD3A3091EE755C800B5B501 /* Video.cpp */; };
4BD424DF2193B5340097291A /* TextureTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD424DD2193B5340097291A /* TextureTarget.cpp */; };
4BD424E02193B5340097291A /* TextureTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD424DD2193B5340097291A /* TextureTarget.cpp */; };
4BD424E52193B5830097291A /* Shader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD424E12193B5820097291A /* Shader.cpp */; };
4BD424E62193B5830097291A /* Shader.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD424E12193B5820097291A /* Shader.cpp */; };
4BD424E72193B5830097291A /* Rectangle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD424E22193B5820097291A /* Rectangle.cpp */; };
4BD424E82193B5830097291A /* Rectangle.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD424E22193B5820097291A /* Rectangle.cpp */; };
4BD468F71D8DF41D0084958B /* 1770.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD468F51D8DF41D0084958B /* 1770.cpp */; };
4BD4A8D01E077FD20020D856 /* PCMTrackTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BD4A8CF1E077FD20020D856 /* PCMTrackTests.mm */; };
4BD5D2682199148100DDF17D /* ScanTargetGLSLFragments.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD5D2672199148100DDF17D /* ScanTargetGLSLFragments.cpp */; };
4BD5D2692199148100DDF17D /* ScanTargetGLSLFragments.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD5D2672199148100DDF17D /* ScanTargetGLSLFragments.cpp */; };
4BD5F1951D13528900631CD1 /* CSBestEffortUpdater.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BD5F1941D13528900631CD1 /* CSBestEffortUpdater.mm */; };
4BD61664206B2AC800236112 /* QuickLoadOptions.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4BD61662206B2AC700236112 /* QuickLoadOptions.xib */; };
4BD67DCB209BE4D700AB2146 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD67DCA209BE4D600AB2146 /* StaticAnalyser.cpp */; };
@ -703,6 +699,7 @@
4B046DC31CFE651500E9E45E /* CRTMachine.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CRTMachine.hpp; sourceTree = "<group>"; };
4B047075201ABC180047AB0D /* Cartridge.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Cartridge.hpp; sourceTree = "<group>"; };
4B049CDC1DA3C82F00322067 /* BCDTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BCDTest.swift; sourceTree = "<group>"; };
4B05401D219D1618001BF69C /* ScanTarget.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = ScanTarget.cpp; path = ../../Outputs/ScanTarget.cpp; sourceTree = "<group>"; };
4B055A6A1FAE763F0060FFFF /* Clock Signal Kiosk */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "Clock Signal Kiosk"; sourceTree = BUILT_PRODUCTS_DIR; };
4B055A771FAE78210060FFFF /* SDL2.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SDL2.framework; path = ../../../../Library/Frameworks/SDL2.framework; sourceTree = SOURCE_ROOT; };
4B055A7C1FAE84A50060FFFF /* main.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = main.cpp; sourceTree = "<group>"; };
@ -715,7 +712,6 @@
4B08A2771EE39306008B7065 /* TestMachine.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = TestMachine.mm; sourceTree = "<group>"; };
4B08A2791EE3957B008B7065 /* TestMachine+ForSubclassEyesOnly.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "TestMachine+ForSubclassEyesOnly.h"; sourceTree = "<group>"; };
4B08A56820D72BEF0016CE5A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = "Clock Signal/Base.lproj/Activity.xib"; sourceTree = SOURCE_ROOT; };
4B0B6E121C9DBD5D00FFB60D /* CRTConstants.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = CRTConstants.hpp; sourceTree = "<group>"; };
4B0CCC421C62D0B3001CAC5F /* CRT.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CRT.cpp; sourceTree = "<group>"; };
4B0CCC431C62D0B3001CAC5F /* CRT.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CRT.hpp; sourceTree = "<group>"; };
4B0E04E81FC9E5DA00F43484 /* CAS.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = CAS.cpp; sourceTree = "<group>"; };
@ -872,8 +868,6 @@
4B4DC8271D2C2470003C5BF8 /* C1540.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = C1540.hpp; sourceTree = "<group>"; };
4B4DC8291D2C27A4003C5BF8 /* SerialBus.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = SerialBus.cpp; sourceTree = "<group>"; };
4B4DC82A1D2C27A4003C5BF8 /* SerialBus.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = SerialBus.hpp; sourceTree = "<group>"; };
4B5073051DDD3B9400C48FBD /* ArrayBuilder.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ArrayBuilder.cpp; sourceTree = "<group>"; };
4B5073061DDD3B9400C48FBD /* ArrayBuilder.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ArrayBuilder.hpp; sourceTree = "<group>"; };
4B5073091DDFCFDF00C48FBD /* ArrayBuilderTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ArrayBuilderTests.mm; sourceTree = "<group>"; };
4B51F70920A521D700AFA2C1 /* Source.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Source.hpp; sourceTree = "<group>"; };
4B51F70A20A521D700AFA2C1 /* Observer.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Observer.hpp; sourceTree = "<group>"; };
@ -1338,8 +1332,6 @@
4BB73EC11B587A5100552FC2 /* Clock_SignalUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Clock_SignalUITests.swift; sourceTree = "<group>"; };
4BB73EC31B587A5100552FC2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
4BB73ECF1B587A6700552FC2 /* Clock Signal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = "Clock Signal.entitlements"; sourceTree = "<group>"; };
4BBB142F1CD2CECE00BDB55C /* IntermediateShader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = IntermediateShader.cpp; sourceTree = "<group>"; };
4BBB14301CD2CECE00BDB55C /* IntermediateShader.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = IntermediateShader.hpp; sourceTree = "<group>"; };
4BBB709C2020109C002FE009 /* DynamicMachine.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = DynamicMachine.hpp; sourceTree = "<group>"; };
4BBB70A2202011C2002FE009 /* MultiMediaTarget.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = MultiMediaTarget.hpp; sourceTree = "<group>"; };
4BBB70A3202011C2002FE009 /* MultiMediaTarget.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MultiMediaTarget.cpp; sourceTree = "<group>"; };
@ -1349,15 +1341,7 @@
4BBC951C1F368D83008F4C34 /* i8272.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = i8272.cpp; path = 8272/i8272.cpp; sourceTree = "<group>"; };
4BBC951D1F368D83008F4C34 /* i8272.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = i8272.hpp; path = 8272/i8272.hpp; sourceTree = "<group>"; };
4BBF49AE1ED2880200AB3669 /* FUSETests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FUSETests.swift; sourceTree = "<group>"; };
4BBF99081C8FBA6F0075DAFB /* TextureBuilder.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TextureBuilder.cpp; sourceTree = "<group>"; };
4BBF99091C8FBA6F0075DAFB /* TextureBuilder.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TextureBuilder.hpp; sourceTree = "<group>"; };
4BBF990A1C8FBA6F0075DAFB /* CRTOpenGL.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = CRTOpenGL.cpp; sourceTree = "<group>"; };
4BBF990B1C8FBA6F0075DAFB /* CRTOpenGL.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = CRTOpenGL.hpp; sourceTree = "<group>"; };
4BBF990E1C8FBA6F0075DAFB /* Flywheel.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Flywheel.hpp; sourceTree = "<group>"; };
4BBF990F1C8FBA6F0075DAFB /* OpenGL.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = OpenGL.hpp; sourceTree = "<group>"; };
4BBF99121C8FBA6F0075DAFB /* TextureTarget.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TextureTarget.cpp; sourceTree = "<group>"; };
4BBF99131C8FBA6F0075DAFB /* TextureTarget.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TextureTarget.hpp; sourceTree = "<group>"; };
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>"; };
@ -1366,17 +1350,11 @@
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>"; };
4BC39567208EE6CF0044766B /* DiskIICard.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = DiskIICard.hpp; sourceTree = "<group>"; };
4BC3B74D1CD194CC00F86E85 /* Shader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Shader.cpp; sourceTree = "<group>"; };
4BC3B74E1CD194CC00F86E85 /* Shader.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Shader.hpp; sourceTree = "<group>"; };
4BC3B7501CD1956900F86E85 /* OutputShader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = OutputShader.cpp; sourceTree = "<group>"; };
4BC3B7511CD1956900F86E85 /* OutputShader.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = OutputShader.hpp; sourceTree = "<group>"; };
4BC5FC2F20CDDDEE00410AA0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = "Clock Signal/Base.lproj/AppleIIOptions.xib"; sourceTree = SOURCE_ROOT; };
4BC751B11D157E61006C31D9 /* 6522Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = 6522Tests.swift; sourceTree = "<group>"; };
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>"; };
@ -1389,12 +1367,22 @@
4BCF1FA21DADC3DD0039D2E7 /* Oric.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Oric.cpp; path = Oric/Oric.cpp; sourceTree = "<group>"; };
4BCF1FA31DADC3DD0039D2E7 /* Oric.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Oric.hpp; path = Oric/Oric.hpp; sourceTree = "<group>"; };
4BD060A51FE49D3C006E14BE /* Speaker.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Speaker.hpp; sourceTree = "<group>"; };
4BD191D9219113B80042E144 /* OpenGL.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = OpenGL.hpp; sourceTree = "<group>"; };
4BD191F22191180E0042E144 /* ScanTarget.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ScanTarget.cpp; sourceTree = "<group>"; };
4BD191F32191180E0042E144 /* ScanTarget.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ScanTarget.hpp; sourceTree = "<group>"; };
4BD388411FE34E010042B588 /* 9918Base.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = 9918Base.hpp; path = 9918/Implementation/9918Base.hpp; sourceTree = "<group>"; };
4BD3A3091EE755C800B5B501 /* Video.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Video.cpp; path = ZX8081/Video.cpp; sourceTree = "<group>"; };
4BD3A30A1EE755C800B5B501 /* Video.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = Video.hpp; path = ZX8081/Video.hpp; sourceTree = "<group>"; };
4BD424DD2193B5340097291A /* TextureTarget.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = TextureTarget.cpp; sourceTree = "<group>"; };
4BD424DE2193B5340097291A /* TextureTarget.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = TextureTarget.hpp; sourceTree = "<group>"; };
4BD424E12193B5820097291A /* Shader.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Shader.cpp; sourceTree = "<group>"; };
4BD424E22193B5820097291A /* Rectangle.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Rectangle.cpp; sourceTree = "<group>"; };
4BD424E32193B5830097291A /* Rectangle.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Rectangle.hpp; sourceTree = "<group>"; };
4BD424E42193B5830097291A /* Shader.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Shader.hpp; sourceTree = "<group>"; };
4BD468F51D8DF41D0084958B /* 1770.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = 1770.cpp; path = 1770/1770.cpp; sourceTree = "<group>"; };
4BD468F61D8DF41D0084958B /* 1770.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = 1770.hpp; path = 1770/1770.hpp; sourceTree = "<group>"; };
4BD4A8CF1E077FD20020D856 /* PCMTrackTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = PCMTrackTests.mm; sourceTree = "<group>"; };
4BD5D2672199148100DDF17D /* ScanTargetGLSLFragments.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = ScanTargetGLSLFragments.cpp; sourceTree = "<group>"; };
4BD5F1931D13528900631CD1 /* CSBestEffortUpdater.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CSBestEffortUpdater.h; path = Updater/CSBestEffortUpdater.h; sourceTree = "<group>"; };
4BD5F1941D13528900631CD1 /* CSBestEffortUpdater.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = CSBestEffortUpdater.mm; path = Updater/CSBestEffortUpdater.mm; sourceTree = "<group>"; };
4BD601A920D89F2A00CBCE57 /* Log.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = Log.hpp; path = ../../Outputs/Log.hpp; sourceTree = "<group>"; };
@ -1454,6 +1442,7 @@
4BF437ED209D0F7E008CBD6B /* SegmentParser.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = SegmentParser.hpp; sourceTree = "<group>"; };
4BF437F0209D112F008CBD6B /* Sector.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Sector.hpp; sourceTree = "<group>"; };
4BF4A2D91F534DB300B171F4 /* TargetPlatforms.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = TargetPlatforms.hpp; sourceTree = "<group>"; };
4BF52672218E752E00313227 /* ScanTarget.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = ScanTarget.hpp; path = ../../Outputs/ScanTarget.hpp; sourceTree = "<group>"; };
4BF6606A1F281573002CB053 /* ClockReceiver.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ClockReceiver.hpp; sourceTree = "<group>"; };
4BF8295F1D8F3C87001BAE39 /* CRC.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = CRC.hpp; path = ../../NumberTheory/CRC.hpp; sourceTree = "<group>"; };
4BFCA1211ECBDCAF00AC40C1 /* AllRAMProcessor.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = AllRAMProcessor.cpp; sourceTree = "<group>"; };
@ -1529,7 +1518,6 @@
children = (
4B0CCC421C62D0B3001CAC5F /* CRT.cpp */,
4B0CCC431C62D0B3001CAC5F /* CRT.hpp */,
4BBF99191C8FC2750075DAFB /* CRTTypes.hpp */,
4BBF99071C8FBA6F0075DAFB /* Internals */,
);
name = CRT;
@ -1791,9 +1779,12 @@
4B366DFD1B5C165F0026627B /* Outputs */ = {
isa = PBXGroup;
children = (
4B0CCC411C62D0B3001CAC5F /* CRT */,
4BD060A41FE49D3C006E14BE /* Speaker */,
4B05401D219D1618001BF69C /* ScanTarget.cpp */,
4BD601A920D89F2A00CBCE57 /* Log.hpp */,
4BF52672218E752E00313227 /* ScanTarget.hpp */,
4B0CCC411C62D0B3001CAC5F /* CRT */,
4BD191D5219113B80042E144 /* OpenGL */,
4BD060A41FE49D3C006E14BE /* Speaker */,
);
name = Outputs;
sourceTree = "<group>";
@ -2939,20 +2930,7 @@
4BBF99071C8FBA6F0075DAFB /* Internals */ = {
isa = PBXGroup;
children = (
4B5073051DDD3B9400C48FBD /* ArrayBuilder.cpp */,
4BBF990A1C8FBA6F0075DAFB /* CRTOpenGL.cpp */,
4BC891AB20F6EAB300EDE5B3 /* Rectangle.cpp */,
4BBF99081C8FBA6F0075DAFB /* TextureBuilder.cpp */,
4BBF99121C8FBA6F0075DAFB /* TextureTarget.cpp */,
4B5073061DDD3B9400C48FBD /* ArrayBuilder.hpp */,
4B0B6E121C9DBD5D00FFB60D /* CRTConstants.hpp */,
4BBF990B1C8FBA6F0075DAFB /* CRTOpenGL.hpp */,
4BBF990E1C8FBA6F0075DAFB /* Flywheel.hpp */,
4BBF990F1C8FBA6F0075DAFB /* OpenGL.hpp */,
4BC891AC20F6EAB300EDE5B3 /* Rectangle.hpp */,
4BBF99091C8FBA6F0075DAFB /* TextureBuilder.hpp */,
4BBF99131C8FBA6F0075DAFB /* TextureTarget.hpp */,
4BC3B74C1CD194CC00F86E85 /* Shaders */,
);
path = Internals;
sourceTree = "<group>";
@ -2966,19 +2944,6 @@
path = "Joystick Manager";
sourceTree = "<group>";
};
4BC3B74C1CD194CC00F86E85 /* Shaders */ = {
isa = PBXGroup;
children = (
4BBB142F1CD2CECE00BDB55C /* IntermediateShader.cpp */,
4BC3B7501CD1956900F86E85 /* OutputShader.cpp */,
4BC3B74D1CD194CC00F86E85 /* Shader.cpp */,
4BBB14301CD2CECE00BDB55C /* IntermediateShader.hpp */,
4BC3B7511CD1956900F86E85 /* OutputShader.hpp */,
4BC3B74E1CD194CC00F86E85 /* Shader.hpp */,
);
path = Shaders;
sourceTree = "<group>";
};
4BC9DF4A1D04691600F44158 /* Components */ = {
isa = PBXGroup;
children = (
@ -3052,6 +3017,19 @@
path = ../../Outputs/Speaker;
sourceTree = "<group>";
};
4BD191D5219113B80042E144 /* OpenGL */ = {
isa = PBXGroup;
children = (
4BD191F22191180E0042E144 /* ScanTarget.cpp */,
4BD5D2672199148100DDF17D /* ScanTargetGLSLFragments.cpp */,
4BD191D9219113B80042E144 /* OpenGL.hpp */,
4BD191F32191180E0042E144 /* ScanTarget.hpp */,
4BD424DC2193B5340097291A /* Primitives */,
);
name = OpenGL;
path = ../../Outputs/OpenGL;
sourceTree = "<group>";
};
4BD388431FE34E060042B588 /* Implementation */ = {
isa = PBXGroup;
children = (
@ -3060,6 +3038,19 @@
name = Implementation;
sourceTree = "<group>";
};
4BD424DC2193B5340097291A /* Primitives */ = {
isa = PBXGroup;
children = (
4BD424E22193B5820097291A /* Rectangle.cpp */,
4BD424E12193B5820097291A /* Shader.cpp */,
4BD424DD2193B5340097291A /* TextureTarget.cpp */,
4BD424E32193B5830097291A /* Rectangle.hpp */,
4BD424E42193B5830097291A /* Shader.hpp */,
4BD424DE2193B5340097291A /* TextureTarget.hpp */,
);
path = Primitives;
sourceTree = "<group>";
};
4BD468F81D8DF4290084958B /* 1770 */ = {
isa = PBXGroup;
children = (
@ -3655,14 +3646,13 @@
4B055A9A1FAE85CB0060FFFF /* MFMDiskController.cpp in Sources */,
4B055ACB1FAE9AFB0060FFFF /* SerialBus.cpp in Sources */,
4B055AA41FAE85E50060FFFF /* DigitalPhaseLockedLoop.cpp in Sources */,
4B055AE61FAE9B6F0060FFFF /* OutputShader.cpp in Sources */,
4B055A9B1FAE85DA0060FFFF /* AcornADF.cpp in Sources */,
4B0E04F11FC9EA9500F43484 /* MSX.cpp in Sources */,
4B055AD51FAE9B0B0060FFFF /* Video.cpp in Sources */,
4B055AE11FAE9B6F0060FFFF /* ArrayBuilder.cpp in Sources */,
4B894521201967B4007DE474 /* StaticAnalyser.cpp in Sources */,
4B7F188F2154825E00388727 /* MasterSystem.cpp in Sources */,
4B055AA51FAE85EF0060FFFF /* Encoder.cpp in Sources */,
4BD5D2692199148100DDF17D /* ScanTargetGLSLFragments.cpp in Sources */,
4B894529201967B4007DE474 /* Disk.cpp in Sources */,
4B055AEA1FAE9B990060FFFF /* 6502Storage.cpp in Sources */,
4B055AA71FAE85EF0060FFFF /* SegmentParser.cpp in Sources */,
@ -3673,10 +3663,8 @@
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 */,
4B7F1898215486A200388727 /* StaticAnalyser.cpp in Sources */,
4B055AE51FAE9B6F0060FFFF /* IntermediateShader.cpp in Sources */,
4B15A9FD208249BB005E6C8D /* StaticAnalyser.cpp in Sources */,
4B055AD31FAE9B0B0060FFFF /* Microdisc.cpp in Sources */,
4B055AB41FAE860F0060FFFF /* OricTAP.cpp in Sources */,
@ -3689,6 +3677,7 @@
4B055A9D1FAE85DA0060FFFF /* D64.cpp in Sources */,
4B055ABB1FAE86170060FFFF /* Oric.cpp in Sources */,
4B12C0EE1FCFAD1A005BFD93 /* Keyboard.cpp in Sources */,
4B05401F219D1618001BF69C /* ScanTarget.cpp in Sources */,
4B055AE81FAE9B7B0060FFFF /* FIRFilter.cpp in Sources */,
4B055A901FAE85A90060FFFF /* TimedEventLoop.cpp in Sources */,
4B055AC71FAE9AEE0060FFFF /* TIA.cpp in Sources */,
@ -3699,8 +3688,8 @@
4B055AAE1FAE85FD0060FFFF /* TrackSerialiser.cpp in Sources */,
4B89452B201967B4007DE474 /* File.cpp in Sources */,
4B055A981FAE85C50060FFFF /* Drive.cpp in Sources */,
4BD424E62193B5830097291A /* Shader.cpp in Sources */,
4B4B1A3D200198CA00A0F866 /* KonamiSCC.cpp in Sources */,
4B055AE21FAE9B6F0060FFFF /* CRTOpenGL.cpp in Sources */,
4B055AC31FAE9AE80060FFFF /* AmstradCPC.cpp in Sources */,
4B055A9E1FAE85DA0060FFFF /* G64.cpp in Sources */,
4B055AB81FAE860F0060FFFF /* ZX80O81P.cpp in Sources */,
@ -3721,7 +3710,6 @@
4B055ADD1FAE9B460060FFFF /* i8272.cpp in Sources */,
4B055AC51FAE9AEE0060FFFF /* Atari2600.cpp in Sources */,
4B055A9C1FAE85DA0060FFFF /* CPCDSK.cpp in Sources */,
4B055AE41FAE9B6F0060FFFF /* TextureTarget.cpp in Sources */,
4B055ABA1FAE86170060FFFF /* Commodore.cpp in Sources */,
4B9BE401203A0C0600FFAE60 /* MultiSpeaker.cpp in Sources */,
4B055AA61FAE85EF0060FFFF /* Parser.cpp in Sources */,
@ -3767,11 +3755,13 @@
4B055ACD1FAE9B030060FFFF /* Keyboard.cpp in Sources */,
4B055AB21FAE860F0060FFFF /* CommodoreTAP.cpp in Sources */,
4B055ADF1FAE9B4C0060FFFF /* IRQDelegatePortHandler.cpp in Sources */,
4BD424E02193B5340097291A /* TextureTarget.cpp in Sources */,
4B055AB51FAE860F0060FFFF /* TapePRG.cpp in Sources */,
4B055AE01FAE9B660060FFFF /* CRT.cpp in Sources */,
4B894527201967B4007DE474 /* StaticAnalyser.cpp in Sources */,
4BAF2B4F2004580C00480230 /* DMK.cpp in Sources */,
4B055AD01FAE9B030060FFFF /* Tape.cpp in Sources */,
4BD424E82193B5830097291A /* Rectangle.cpp in Sources */,
4B055A961FAE85BB0060FFFF /* Commodore.cpp in Sources */,
4B0333B02094081A0050B93D /* AppleDSK.cpp in Sources */,
4B055ADE1FAE9B4C0060FFFF /* 6522Base.cpp in Sources */,
@ -3783,12 +3773,11 @@
4B894537201967B4007DE474 /* Z80.cpp in Sources */,
4B055A9F1FAE85DA0060FFFF /* HFE.cpp in Sources */,
4B07835B1FC11D42001D12BB /* Configurable.cpp in Sources */,
4B055AE71FAE9B6F0060FFFF /* Shader.cpp in Sources */,
4BD191F52191180E0042E144 /* ScanTarget.cpp in Sources */,
4B894523201967B4007DE474 /* StaticAnalyser.cpp in Sources */,
4B055AEC1FAE9BA20060FFFF /* Z80Base.cpp in Sources */,
4B0F94FF208C1A1600FE41D9 /* NIB.cpp in Sources */,
4B0E04EB1FC9E78800F43484 /* CAS.cpp in Sources */,
4B055AE31FAE9B6F0060FFFF /* TextureBuilder.cpp in Sources */,
4BB0A65D2045009000FB3688 /* ColecoVision.cpp in Sources */,
4BB0A65C2044FD3000FB3688 /* SN76489.cpp in Sources */,
4B595FAE2086DFBA0083CAA8 /* AudioToggle.cpp in Sources */,
@ -3821,8 +3810,8 @@
4B58601E1F806AB200AEE2E3 /* MFMSectorDump.cpp in Sources */,
4B448E841F1C4C480009ABD6 /* PulseQueuedTape.cpp in Sources */,
4B0E61071FF34737002A9DBD /* MSX.cpp in Sources */,
4BBF99151C8FBA6F0075DAFB /* CRTOpenGL.cpp in Sources */,
4B4518A01F75FD1C00926311 /* CPCDSK.cpp in Sources */,
4BD424DF2193B5340097291A /* TextureTarget.cpp in Sources */,
4B0CCC451C62D0B3001CAC5F /* CRT.cpp in Sources */,
4B322E041F5A2E3C004EB04C /* Z80Base.cpp in Sources */,
4B894530201967B4007DE474 /* StaticAnalyser.cpp in Sources */,
@ -3843,6 +3832,7 @@
4B1497921EE4B5A800CE2596 /* ZX8081.cpp in Sources */,
4B643F3F1D77B88000D431D6 /* DocumentController.swift in Sources */,
4BC39568208EE6CF0044766B /* DiskIICard.cpp in Sources */,
4B05401E219D1618001BF69C /* ScanTarget.cpp in Sources */,
4B4518861F75E91A00926311 /* MFMDiskController.cpp in Sources */,
4B54C0BF1F8D8F450050900F /* Keyboard.cpp in Sources */,
4B3FE75E1F3CF68B00448EE4 /* CPM.cpp in Sources */,
@ -3850,14 +3840,14 @@
4B4DC82B1D2C27A4003C5BF8 /* SerialBus.cpp in Sources */,
4BBFFEE61F7B27F1005F3FEB /* TrackSerialiser.cpp in Sources */,
4BAE49582032881E004BE78E /* CSZX8081.mm in Sources */,
4BC3B74F1CD194CC00F86E85 /* Shader.cpp in Sources */,
4BD424E52193B5830097291A /* Shader.cpp in Sources */,
4B0333AF2094081A0050B93D /* AppleDSK.cpp in Sources */,
4B894518201967B4007DE474 /* ConfidenceCounter.cpp in Sources */,
4B89452E201967B4007DE474 /* StaticAnalyser.cpp in Sources */,
4BD5D2682199148100DDF17D /* ScanTargetGLSLFragments.cpp in Sources */,
4B38F3481F2EC11D00D9235D /* AmstradCPC.cpp in Sources */,
4B8FE2221DA19FB20090D3CE /* MachinePanel.swift in Sources */,
4B4518A41F75FD1C00926311 /* OricMFMDSK.cpp in Sources */,
4BBB14311CD2CECE00BDB55C /* IntermediateShader.cpp in Sources */,
4B4B1A3C200198CA00A0F866 /* KonamiSCC.cpp in Sources */,
4BB0A65B2044FD3000FB3688 /* SN76489.cpp in Sources */,
4BD5F1951D13528900631CD1 /* CSBestEffortUpdater.mm in Sources */,
@ -3865,7 +3855,6 @@
4BDB61EC203285AE0048AF91 /* Atari2600OptionsPanel.swift in Sources */,
4BBB70A8202014E2002FE009 /* MultiCRTMachine.cpp in Sources */,
4B8805F71DCFF6C9003085B1 /* Commodore.cpp in Sources */,
4BBF99181C8FBA6F0075DAFB /* TextureTarget.cpp in Sources */,
4BC76E691C98E31700E6EF73 /* FIRFilter.cpp in Sources */,
4B3BF5B01F146265005B6C36 /* CSW.cpp in Sources */,
4B4518A51F75FD1C00926311 /* SSD.cpp in Sources */,
@ -3892,6 +3881,7 @@
4B4518851F75E91A00926311 /* DiskController.cpp in Sources */,
4B8334841F5DA0360097E338 /* Z80Storage.cpp in Sources */,
4BA61EB01D91515900B3C876 /* NSData+StdVector.mm in Sources */,
4BD191F42191180E0042E144 /* ScanTarget.cpp in Sources */,
4B0F94FE208C1A1600FE41D9 /* NIB.cpp in Sources */,
4B89452A201967B4007DE474 /* File.cpp in Sources */,
4B4DC8211D2C2425003C5BF8 /* Vic20.cpp in Sources */,
@ -3901,9 +3891,7 @@
4BD468F71D8DF41D0084958B /* 1770.cpp in Sources */,
4B7F1897215486A200388727 /* StaticAnalyser.cpp in Sources */,
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 */,
@ -3949,8 +3937,6 @@
4B7136891F78725F008B8ED9 /* Shifter.cpp in Sources */,
4BDB61EB2032806E0048AF91 /* CSAtari2600.mm in Sources */,
4BFDD78C1F7F2DB4008579B9 /* ImplicitSectors.cpp in Sources */,
4BC3B7521CD1956900F86E85 /* OutputShader.cpp in Sources */,
4B5073071DDD3B9400C48FBD /* ArrayBuilder.cpp in Sources */,
4B894526201967B4007DE474 /* StaticAnalyser.cpp in Sources */,
4BEE0A6F1D72496600532C7B /* Cartridge.cpp in Sources */,
4B8805FB1DCFF807003085B1 /* Oric.cpp in Sources */,
@ -3971,6 +3957,7 @@
4B89453E201967B4007DE474 /* StaticAnalyser.cpp in Sources */,
4B37EE821D7345A6006A09A4 /* BinaryDump.cpp in Sources */,
4B8334821F5D9FF70097E338 /* PartialMachineCycle.cpp in Sources */,
4BD424E72193B5830097291A /* Rectangle.cpp in Sources */,
4B1B88C0202E3DB200B67DFF /* MultiConfigurable.cpp in Sources */,
4B54C0BC1F8D8E790050900F /* KeyboardMachine.cpp in Sources */,
4BB73EA21B587A5100552FC2 /* AppDelegate.swift in Sources */,

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14113" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14113"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -13,37 +13,43 @@
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Options" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="ZW7-Bw-4RP" customClass="MachinePanel" customModule="Clock_Signal" customModuleProvider="target">
<window title="Options" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="ZW7-Bw-4RP" customClass="MachinePanel" customModule="Clock_Signal" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" utility="YES" nonactivatingPanel="YES" HUD="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="80" y="150" width="200" height="54"/>
<rect key="contentRect" x="80" y="150" width="200" height="61"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="900"/>
<view key="contentView" id="tpZ-0B-QQu">
<rect key="frame" x="0.0" y="0.0" width="200" height="54"/>
<rect key="frame" x="0.0" y="0.0" width="200" height="61"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button translatesAutoresizingMaskIntoConstraints="NO" id="e1J-pw-zGw">
<rect key="frame" x="18" y="18" width="164" height="18"/>
<buttonCell key="cell" type="check" title="Accelerate DOS 3.3" bezelStyle="regularSquare" imagePosition="left" alignment="left" state="on" inset="2" id="tD6-UB-ESB">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ex3-VM-58z">
<rect key="frame" x="18" y="17" width="165" height="25"/>
<popUpButtonCell key="cell" type="push" title="Colour" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="1" imageScaling="proportionallyDown" inset="2" selectedItem="gOu-dv-tre" id="u3N-Je-c2L">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
<menu key="menu" id="BUS-Pl-jBm">
<items>
<menuItem title="Colour" state="on" tag="1" id="gOu-dv-tre"/>
<menuItem title="Monochrome" tag="3" id="qhQ-ab-jRo"/>
</items>
</menu>
</popUpButtonCell>
<connections>
<action selector="setFastLoading:" target="ZW7-Bw-4RP" id="JmG-Ks-jSh"/>
<action selector="setDisplayType:" target="ZW7-Bw-4RP" id="f7A-2O-wR8"/>
</connections>
</button>
</popUpButton>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="e1J-pw-zGw" secondAttribute="bottom" constant="20" id="5ce-DO-a4T"/>
<constraint firstItem="e1J-pw-zGw" firstAttribute="leading" secondItem="tpZ-0B-QQu" secondAttribute="leading" constant="20" id="HSD-3d-Bl7"/>
<constraint firstAttribute="trailing" secondItem="e1J-pw-zGw" secondAttribute="trailing" constant="20" id="Q9M-FH-92N"/>
<constraint firstItem="e1J-pw-zGw" firstAttribute="top" secondItem="tpZ-0B-QQu" secondAttribute="top" constant="20" id="ul9-lf-Y3u"/>
<constraint firstAttribute="bottom" secondItem="ex3-VM-58z" secondAttribute="bottom" constant="20" id="4ZS-q5-TJL"/>
<constraint firstItem="ex3-VM-58z" firstAttribute="leading" secondItem="tpZ-0B-QQu" secondAttribute="leading" constant="20" id="8Pj-Ns-TrJ"/>
<constraint firstAttribute="trailing" secondItem="ex3-VM-58z" secondAttribute="trailing" constant="20" id="QWA-lY-ugz"/>
<constraint firstItem="ex3-VM-58z" firstAttribute="top" secondItem="tpZ-0B-QQu" secondAttribute="top" constant="20" id="szw-WO-3tS"/>
</constraints>
</view>
<connections>
<outlet property="fastLoadingButton" destination="e1J-pw-zGw" id="jj7-OZ-mOH"/>
<outlet property="displayTypeButton" destination="ex3-VM-58z" id="lmZ-aN-lcj"/>
</connections>
<point key="canvasLocation" x="175" y="30"/>
<point key="canvasLocation" x="-161" y="38.5"/>
</window>
</objects>
</document>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14113" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14113"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
@ -13,17 +13,17 @@
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<window title="Options" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="ZW7-Bw-4RP" customClass="MachinePanel" customModule="Clock_Signal" customModuleProvider="target">
<window title="Options" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" hidesOnDeactivate="YES" releasedWhenClosed="NO" visibleAtLaunch="NO" frameAutosaveName="" animationBehavior="default" id="ZW7-Bw-4RP" customClass="MachinePanel" customModule="Clock_Signal" customModuleProvider="target">
<windowStyleMask key="styleMask" titled="YES" closable="YES" utility="YES" nonactivatingPanel="YES" HUD="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="80" y="150" width="200" height="83"/>
<rect key="screenRect" x="0.0" y="0.0" width="1440" height="900"/>
<view key="contentView" id="tpZ-0B-QQu">
<rect key="frame" x="0.0" y="0.0" width="200" height="83"/>
<rect key="frame" x="0.0" y="0.0" width="222" height="83"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button translatesAutoresizingMaskIntoConstraints="NO" id="zPG-yW-4Gy">
<rect key="frame" x="18" y="47" width="164" height="18"/>
<rect key="frame" x="18" y="47" width="186" height="18"/>
<buttonCell key="cell" type="check" title="Load Quickly" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="alI-Mw-35c">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="system"/>
@ -33,7 +33,7 @@
</connections>
</button>
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="rh8-km-57n">
<rect key="frame" x="18" y="17" width="165" height="26"/>
<rect key="frame" x="18" y="17" width="187" height="25"/>
<popUpButtonCell key="cell" type="push" title="SCART" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="tJM-kX-gaK" id="8SX-c5-ud1">
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="menu"/>
@ -41,6 +41,7 @@
<items>
<menuItem title="SCART" state="on" id="tJM-kX-gaK"/>
<menuItem title="Composite" tag="1" id="fFm-fS-rWG"/>
<menuItem title="Composite Monochrome" tag="3" id="hjs-qh-WKP"/>
</items>
</menu>
</popUpButtonCell>

View File

@ -32,6 +32,7 @@ class MachinePanel: NSPanel {
switch tag {
case 1: return .composite
case 2: return .sVideo
case 3: return .monochromeComposite
default: break
}
return .RGB

View File

@ -24,7 +24,8 @@
typedef NS_ENUM(NSInteger, CSMachineVideoSignal) {
CSMachineVideoSignalComposite,
CSMachineVideoSignalSVideo,
CSMachineVideoSignalRGB
CSMachineVideoSignalRGB,
CSMachineVideoSignalMonochromeComposite
};
typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {

View File

@ -26,6 +26,11 @@
#include <bitset>
#import <OpenGL/OpenGL.h>
#include <OpenGL/gl3.h>
#include "../../../../Outputs/OpenGL/ScanTarget.hpp"
@interface CSMachine() <CSFastLoading>
- (void)speaker:(Outputs::Speaker::Speaker *)speaker didCompleteSamples:(const int16_t *)samples length:(int)length;
- (void)speakerDidChangeInputClock:(Outputs::Speaker::Speaker *)speaker;
@ -80,6 +85,8 @@ struct ActivityObserver: public Activity::Observer {
CSJoystickManager *_joystickManager;
std::bitset<65536> _depressedKeys;
NSMutableArray<NSString *> *_leds;
std::unique_ptr<Outputs::Display::OpenGL::ScanTarget> _scanTarget;
}
- (instancetype)initWithAnalyser:(CSStaticAnalyser *)result {
@ -133,7 +140,7 @@ struct ActivityObserver: public Activity::Observer {
[_view performWithGLContext:^{
@synchronized(self) {
self->_machine->crt_machine()->close_output();
self->_scanTarget.reset();
}
}];
}
@ -178,7 +185,7 @@ struct ActivityObserver: public Activity::Observer {
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;
++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.
@ -228,15 +235,12 @@ struct ActivityObserver: public Activity::Observer {
}
- (void)setupOutputWithAspectRatio:(float)aspectRatio {
_machine->crt_machine()->setup_output(aspectRatio);
// Since OS X v10.6, Macs have had a gamma of 2.2.
_machine->crt_machine()->get_crt()->set_output_gamma(2.2f);
_machine->crt_machine()->get_crt()->set_target_framebuffer(0);
_scanTarget.reset(new Outputs::Display::OpenGL::ScanTarget);
_machine->crt_machine()->set_scan_target(_scanTarget.get());
}
- (void)drawViewForPixelSize:(CGSize)pixelSize onlyIfDirty:(BOOL)onlyIfDirty {
_machine->crt_machine()->get_crt()->draw_frame((unsigned int)pixelSize.width, (unsigned int)pixelSize.height, onlyIfDirty ? true : false);
_scanTarget->draw(true, (int)pixelSize.width, (int)pixelSize.height);
}
- (void)paste:(NSString *)paste {
@ -357,7 +361,7 @@ struct ActivityObserver: public Activity::Observer {
BIND(VK_ForwardDelete, Delete);
BIND(VK_LeftArrow, Left); BIND(VK_RightArrow, Right);
BIND(VK_DownArrow, Down); BIND(VK_UpArrow, Up);
BIND(VK_DownArrow, Down); BIND(VK_UpArrow, Up);
}
#undef BIND
@ -457,9 +461,10 @@ struct ActivityObserver: public Activity::Observer {
Configurable::SelectionSet selection_set;
Configurable::Display display;
switch(videoSignal) {
case CSMachineVideoSignalRGB: display = Configurable::Display::RGB; break;
case CSMachineVideoSignalSVideo: display = Configurable::Display::SVideo; break;
case CSMachineVideoSignalComposite: display = Configurable::Display::Composite; break;
case CSMachineVideoSignalRGB: display = Configurable::Display::RGB; break;
case CSMachineVideoSignalSVideo: display = Configurable::Display::SVideo; break;
case CSMachineVideoSignalComposite: display = Configurable::Display::CompositeColour; break;
case CSMachineVideoSignalMonochromeComposite: display = Configurable::Display::CompositeMonochrome; break;
}
append_display_selection(selection_set, display);
configurable_device->set_selections(selection_set);
@ -479,9 +484,10 @@ struct ActivityObserver: public Activity::Observer {
// Get the standard option for this video signal.
Configurable::StandardOptions option;
switch(videoSignal) {
case CSMachineVideoSignalRGB: option = Configurable::DisplayRGB; break;
case CSMachineVideoSignalSVideo: option = Configurable::DisplaySVideo; break;
case CSMachineVideoSignalComposite: option = Configurable::DisplayComposite; break;
case CSMachineVideoSignalRGB: option = Configurable::DisplayRGB; break;
case CSMachineVideoSignalSVideo: option = Configurable::DisplaySVideo; break;
case CSMachineVideoSignalComposite: option = Configurable::DisplayCompositeColour; break;
case CSMachineVideoSignalMonochromeComposite: option = Configurable::DisplayCompositeMonochrome; break;
}
std::unique_ptr<Configurable::Option> display_option = std::move(standard_options(option).front());
Configurable::ListOption *display_list_option = dynamic_cast<Configurable::ListOption *>(display_option.get());

View File

@ -169,7 +169,7 @@ static Analyser::Static::ZX8081::Target::MemoryModel ZX8081MemoryModelFromSize(K
std::unique_ptr<Target> target(new Target);
target->machine = Analyser::Machine::AppleII;
switch(model) {
default: target->model = Target::Model::II; break;
default: target->model = Target::Model::II; break;
case CSMachineAppleIIModelAppleIIPlus: target->model = Target::Model::IIplus; break;
case CSMachineAppleIIModelAppleIIe: target->model = Target::Model::IIe; break;
case CSMachineAppleIIModelAppleEnhancedIIe: target->model = Target::Model::EnhancedIIe; break;
@ -189,7 +189,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::MasterSystem: return @"CompositeOptions";

View File

@ -141,7 +141,7 @@ class MachinePicker: NSObject {
case 13: diskController = .thirteenSector
case 16: diskController = .sixteenSector
case 0: fallthrough
default: diskController = .none
default: diskController = .none
}
return CSStaticAnalyser(appleIIModel: model, diskController: diskController)
@ -162,7 +162,7 @@ class MachinePicker: NSObject {
switch oricDiskInterfaceButton!.selectedTag() {
case 1: diskInterface = .microdisc
case 2: diskInterface = .pravetz
default: break;
default: break;
}
var model: CSMachineOricModel = .oric1

View File

@ -93,9 +93,9 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
{
NSOpenGLPFADoubleBuffer,
NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion3_2Core,
NSOpenGLPFAMultisample,
NSOpenGLPFASampleBuffers, 1,
NSOpenGLPFASamples, 2,
// NSOpenGLPFAMultisample,
// NSOpenGLPFASampleBuffers, 1,
// NSOpenGLPFASamples, 2,
0
};

View File

@ -19,7 +19,7 @@
}
- (void)setUp {
[super setUp];
[super setUp];
// Create a valid OpenGL context, so that a VDP can be constructed.
NSOpenGLPixelFormatAttribute attributes[] =
@ -34,10 +34,10 @@
}
- (void)tearDown {
// Put teardown code here. This method is called after the invocation of each test method in the class.
_openGLContext = nil;
// Put teardown code here. This method is called after the invocation of each test method in the class.
_openGLContext = nil;
[super tearDown];
[super tearDown];
}
- (void)testLineInterrupt {

View File

@ -303,7 +303,7 @@ class Z80MachineCycleTests: XCTestCase {
MachineCycle(operation: .readOpcode, length: 4),
MachineCycle(operation: .readOpcode, length: 4),
MachineCycle(operation: .read, length: 3),
MachineCycle(operation: .read, length: 3),
MachineCycle(operation: .read, length: 3),
MachineCycle(operation: .read, length: 3),
MachineCycle(operation: .read, length: 3),
]
@ -318,7 +318,7 @@ class Z80MachineCycleTests: XCTestCase {
MachineCycle(operation: .readOpcode, length: 4),
MachineCycle(operation: .readOpcode, length: 4),
MachineCycle(operation: .read, length: 3),
MachineCycle(operation: .read, length: 3),
MachineCycle(operation: .read, length: 3),
MachineCycle(operation: .write, length: 3),
MachineCycle(operation: .write, length: 3),
]
@ -333,7 +333,7 @@ class Z80MachineCycleTests: XCTestCase {
MachineCycle(operation: .readOpcode, length: 4),
MachineCycle(operation: .readOpcode, length: 4),
MachineCycle(operation: .read, length: 3),
MachineCycle(operation: .read, length: 3),
MachineCycle(operation: .read, length: 3),
MachineCycle(operation: .read, length: 3),
MachineCycle(operation: .read, length: 3),
]
@ -348,7 +348,7 @@ class Z80MachineCycleTests: XCTestCase {
MachineCycle(operation: .readOpcode, length: 4),
MachineCycle(operation: .readOpcode, length: 4),
MachineCycle(operation: .read, length: 3),
MachineCycle(operation: .read, length: 3),
MachineCycle(operation: .read, length: 3),
MachineCycle(operation: .write, length: 3),
MachineCycle(operation: .write, length: 3),
]

View File

@ -61,9 +61,10 @@ SOURCES += glob.glob('../../Machines/Oric/*.cpp')
SOURCES += glob.glob('../../Machines/Utility/*.cpp')
SOURCES += glob.glob('../../Machines/ZX8081/*.cpp')
SOURCES += glob.glob('../../Outputs/*.cpp')
SOURCES += glob.glob('../../Outputs/CRT/*.cpp')
SOURCES += glob.glob('../../Outputs/CRT/Internals/*.cpp')
SOURCES += glob.glob('../../Outputs/CRT/Internals/Shaders/*.cpp')
SOURCES += glob.glob('../../Outputs/OpenGL/*.cpp')
SOURCES += glob.glob('../../Outputs/OpenGL/Primitives/*.cpp')
SOURCES += glob.glob('../../Processors/6502/Implementation/*.cpp')
SOURCES += glob.glob('../../Processors/Z80/Implementation/*.cpp')

View File

@ -26,7 +26,8 @@
#include "../../Concurrency/BestEffortUpdater.hpp"
#include "../../Activity/Observer.hpp"
#include "../../Outputs/CRT/Internals/Rectangle.hpp"
#include "../../Outputs/OpenGL/Primitives/Rectangle.hpp"
#include "../../Outputs/OpenGL/ScanTarget.hpp"
namespace {
@ -109,7 +110,7 @@ class ActivityObserver: public Activity::Observer {
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))));
lights_.emplace(std::make_pair(drive, std::unique_ptr<Outputs::Display::OpenGL::Rectangle>(new Outputs::Display::OpenGL::Rectangle(right_x, y, width, height))));
y -= height * 2.0f;
}
@ -154,7 +155,7 @@ class ActivityObserver: public Activity::Observer {
blinking_leds_.insert(name);
}
std::map<std::string, std::unique_ptr<OpenGL::Rectangle>> lights_;
std::map<std::string, std::unique_ptr<Outputs::Display::OpenGL::Rectangle>> lights_;
std::set<std::string> lit_leds_;
std::set<std::string> blinking_leds_;
};
@ -454,22 +455,24 @@ int main(int argc, char *argv[]) {
400, 300,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
if(!window)
{
std::cerr << "Could not create window" << std::endl;
SDL_GLContext gl_context;
if(window) {
gl_context = SDL_GL_CreateContext(window);
}
if(!window || !gl_context) {
std::cerr << "Could not create " << (window ? "OpenGL context" : "window");
std::cerr << "; reported error: \"" << SDL_GetError() << "\"" << std::endl;
return -1;
}
SDL_GLContext gl_context = SDL_GL_CreateContext(window);
SDL_GL_MakeCurrent(window, gl_context);
GLint target_framebuffer = 0;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &target_framebuffer);
// Setup output, assuming a CRT machine for now, and prepare a best-effort updater.
machine->crt_machine()->setup_output(4.0 / 3.0);
machine->crt_machine()->get_crt()->set_output_gamma(2.2f);
machine->crt_machine()->get_crt()->set_target_framebuffer(target_framebuffer);
Outputs::Display::OpenGL::ScanTarget scan_target(target_framebuffer);
machine->crt_machine()->set_scan_target(&scan_target);
// For now, lie about audio output intentions.
auto speaker = machine->crt_machine()->get_speaker();
@ -593,7 +596,7 @@ int main(int argc, char *argv[]) {
case SDL_WINDOWEVENT_RESIZED: {
GLint target_framebuffer = 0;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &target_framebuffer);
machine->crt_machine()->get_crt()->set_target_framebuffer(target_framebuffer);
scan_target.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;
@ -785,7 +788,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);
scan_target.draw(true, int(window_width), int(window_height));
if(activity_observer) activity_observer->draw();
SDL_GL_SwapWindow(window);
}

View File

@ -7,7 +7,7 @@
//
#include "CRT.hpp"
#include "Internals/CRTOpenGL.hpp"
#include <cstdarg>
#include <cmath>
#include <algorithm>
@ -15,31 +15,30 @@
using namespace Outputs::CRT;
void CRT::set_new_timing(unsigned int cycles_per_line, unsigned int height_of_display, ColourSpace colour_space, unsigned int colour_cycle_numerator, unsigned int colour_cycle_denominator, unsigned int vertical_sync_half_lines, bool should_alternate) {
openGL_output_builder_.set_colour_format(colour_space, colour_cycle_numerator, colour_cycle_denominator);
void CRT::set_new_timing(int cycles_per_line, int height_of_display, Outputs::Display::ColourSpace colour_space, int colour_cycle_numerator, int colour_cycle_denominator, int vertical_sync_half_lines, bool should_alternate) {
const unsigned int millisecondsHorizontalRetraceTime = 7; // source: Dictionary of Video and Television Technology, p. 234
const unsigned int scanlinesVerticalRetraceTime = 8; // source: ibid
const int millisecondsHorizontalRetraceTime = 7; // Source: Dictionary of Video and Television Technology, p. 234.
const int scanlinesVerticalRetraceTime = 8; // Source: ibid.
// To quote:
//
// "retrace interval; The interval of time for the return of the blanked scanning beam of
// a TV picture tube or camera tube to the starting point of a line or field. It is about
// 7 microseconds for horizontal retrace and 500 to 750 microseconds for vertical retrace
// in NTSC and PAL TV."
// To quote:
//
// "retrace interval; The interval of time for the return of the blanked scanning beam of
// a TV picture tube or camera tube to the starting point of a line or field. It is about
// 7 microseconds for horizontal retrace and 500 to 750 microseconds for vertical retrace
// in NTSC and PAL TV."
time_multiplier_ = IntermediateBufferWidth / cycles_per_line;
phase_denominator_ = cycles_per_line * colour_cycle_denominator * time_multiplier_;
time_multiplier_ = 65535 / cycles_per_line;
phase_denominator_ = int64_t(cycles_per_line) * int64_t(colour_cycle_denominator) * int64_t(time_multiplier_);
phase_numerator_ = 0;
colour_cycle_numerator_ = colour_cycle_numerator;
colour_cycle_numerator_ = int64_t(colour_cycle_numerator);
phase_alternates_ = should_alternate;
is_alernate_line_ &= phase_alternates_;
cycles_per_line_ = cycles_per_line;
unsigned int multiplied_cycles_per_line = cycles_per_line * time_multiplier_;
const int multiplied_cycles_per_line = cycles_per_line * time_multiplier_;
// allow sync to be detected (and acted upon) a line earlier than the specified requirement,
// Allow sync to be detected (and acted upon) a line earlier than the specified requirement,
// as a simple way of avoiding not-quite-exact comparison issues while still being true enough to
// the gist for simple debugging
// the gist for simple debugging.
sync_capacitor_charge_threshold_ = ((vertical_sync_half_lines - 2) * cycles_per_line) >> 1;
// Create the two flywheels:
@ -54,23 +53,66 @@ void CRT::set_new_timing(unsigned int cycles_per_line, unsigned int height_of_di
horizontal_flywheel_.reset(new Flywheel(multiplied_cycles_per_line, (millisecondsHorizontalRetraceTime * multiplied_cycles_per_line) >> 6, multiplied_cycles_per_line >> 5));
vertical_flywheel_.reset(new Flywheel(multiplied_cycles_per_line * height_of_display, scanlinesVerticalRetraceTime * multiplied_cycles_per_line, (multiplied_cycles_per_line * height_of_display) >> 3));
// figure out the divisor necessary to get the horizontal flywheel into a 16-bit range
unsigned int real_clock_scan_period = (multiplied_cycles_per_line * height_of_display) / (time_multiplier_ * common_output_divisor_);
vertical_flywheel_output_divider_ = static_cast<uint16_t>(ceilf(real_clock_scan_period / 65536.0f) * (time_multiplier_ * common_output_divisor_));
// Figure out the divisor necessary to get the horizontal flywheel into a 16-bit range.
const int real_clock_scan_period = vertical_flywheel_->get_scan_period();
vertical_flywheel_output_divider_ = (real_clock_scan_period + 65534) / 65535;
openGL_output_builder_.set_timing(cycles_per_line, multiplied_cycles_per_line, height_of_display, horizontal_flywheel_->get_scan_period(), vertical_flywheel_->get_scan_period(), vertical_flywheel_output_divider_);
// Communicate relevant fields to the scan target.
scan_target_modals_.output_scale.x = uint16_t(horizontal_flywheel_->get_scan_period());
scan_target_modals_.output_scale.y = uint16_t(real_clock_scan_period / vertical_flywheel_output_divider_);
scan_target_modals_.expected_vertical_lines = height_of_display;
scan_target_modals_.composite_colour_space = colour_space;
scan_target_modals_.colour_cycle_numerator = colour_cycle_numerator;
scan_target_modals_.colour_cycle_denominator = colour_cycle_denominator;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_new_display_type(unsigned int cycles_per_line, DisplayType displayType) {
void CRT::set_scan_target(Outputs::Display::ScanTarget *scan_target) {
scan_target_ = scan_target;
if(!scan_target_) scan_target_ = &Outputs::Display::NullScanTarget::singleton;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_new_data_type(Outputs::Display::InputDataType data_type) {
scan_target_modals_.input_data_type = data_type;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_visible_area(Outputs::Display::Rect visible_area) {
scan_target_modals_.visible_area = visible_area;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_display_type(Outputs::Display::DisplayType display_type) {
scan_target_modals_.display_type = display_type;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_phase_linked_luminance_offset(float offset) {
scan_target_modals_.input_data_tweaks.phase_linked_luminance_offset = offset;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_input_data_type(Outputs::Display::InputDataType input_data_type) {
scan_target_modals_.input_data_type = input_data_type;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_brightness(float brightness) {
scan_target_modals_.brightness = brightness;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_new_display_type(int cycles_per_line, Outputs::Display::Type displayType) {
switch(displayType) {
case DisplayType::PAL50:
set_new_timing(cycles_per_line, 312, ColourSpace::YUV, 709379, 2500, 5, true); // i.e. 283.7516; 2.5 lines = vertical sync
set_input_gamma(2.8f);
case Outputs::Display::Type::PAL50:
scan_target_modals_.intended_gamma = 2.8f;
set_new_timing(cycles_per_line, 312, Outputs::Display::ColourSpace::YUV, 709379, 2500, 5, true); // i.e. 283.7516 colour cycles per line; 2.5 lines = vertical sync.
break;
case DisplayType::NTSC60:
set_new_timing(cycles_per_line, 262, ColourSpace::YIQ, 455, 2, 6, false); // i.e. 227.5, 3 lines = vertical sync
set_input_gamma(2.2f);
case Outputs::Display::Type::NTSC60:
scan_target_modals_.intended_gamma = 2.2f;
set_new_timing(cycles_per_line, 262, Outputs::Display::ColourSpace::YIQ, 455, 2, 6, false); // i.e. 227.5 colour cycles per line, 3 lines = vertical sync.
break;
}
}
@ -84,175 +126,145 @@ void CRT::set_composite_function_type(CompositeSourceType type, float offset_of_
}
void CRT::set_input_gamma(float gamma) {
input_gamma_ = gamma;
update_gamma();
scan_target_modals_.intended_gamma = gamma;
scan_target_->set_modals(scan_target_modals_);
}
void CRT::set_output_gamma(float gamma) {
output_gamma_ = gamma;
update_gamma();
}
void CRT::update_gamma() {
float gamma_ratio = input_gamma_ / output_gamma_;
openGL_output_builder_.set_gamma(gamma_ratio);
}
CRT::CRT(unsigned int common_output_divisor, unsigned int buffer_depth) :
common_output_divisor_(common_output_divisor),
openGL_output_builder_(buffer_depth) {}
CRT::CRT( unsigned int cycles_per_line,
unsigned int common_output_divisor,
unsigned int height_of_display,
ColourSpace colour_space,
unsigned int colour_cycle_numerator, unsigned int colour_cycle_denominator,
unsigned int vertical_sync_half_lines,
CRT::CRT( int cycles_per_line,
int clocks_per_pixel_greatest_common_divisor,
int height_of_display,
Outputs::Display::ColourSpace colour_space,
int colour_cycle_numerator, int colour_cycle_denominator,
int vertical_sync_half_lines,
bool should_alternate,
unsigned int buffer_depth) :
CRT(common_output_divisor, buffer_depth) {
Outputs::Display::InputDataType data_type) {
scan_target_modals_.input_data_type = data_type;
scan_target_modals_.cycles_per_line = cycles_per_line;
scan_target_modals_.clocks_per_pixel_greatest_common_divisor = clocks_per_pixel_greatest_common_divisor;
set_new_timing(cycles_per_line, height_of_display, colour_space, colour_cycle_numerator, colour_cycle_denominator, vertical_sync_half_lines, should_alternate);
}
CRT::CRT(unsigned int cycles_per_line, unsigned int common_output_divisor, DisplayType displayType, unsigned int buffer_depth) :
CRT(common_output_divisor, buffer_depth) {
set_new_display_type(cycles_per_line, displayType);
CRT::CRT( int cycles_per_line,
int clocks_per_pixel_greatest_common_divisor,
Outputs::Display::Type display_type,
Outputs::Display::InputDataType data_type) {
scan_target_modals_.input_data_type = data_type;
scan_target_modals_.cycles_per_line = cycles_per_line;
scan_target_modals_.clocks_per_pixel_greatest_common_divisor = clocks_per_pixel_greatest_common_divisor;
set_new_display_type(cycles_per_line, display_type);
}
// MARK: - Sync loop
Flywheel::SyncEvent CRT::get_next_vertical_sync_event(bool vsync_is_requested, unsigned int cycles_to_run_for, unsigned int *cycles_advanced) {
Flywheel::SyncEvent CRT::get_next_vertical_sync_event(bool vsync_is_requested, int cycles_to_run_for, int *cycles_advanced) {
return vertical_flywheel_->get_next_event_in_period(vsync_is_requested, cycles_to_run_for, cycles_advanced);
}
Flywheel::SyncEvent CRT::get_next_horizontal_sync_event(bool hsync_is_requested, unsigned int cycles_to_run_for, unsigned int *cycles_advanced) {
Flywheel::SyncEvent CRT::get_next_horizontal_sync_event(bool hsync_is_requested, int cycles_to_run_for, int *cycles_advanced) {
return horizontal_flywheel_->get_next_event_in_period(hsync_is_requested, cycles_to_run_for, cycles_advanced);
}
#define output_x1() (*reinterpret_cast<uint16_t *>(&next_output_run[OutputVertexOffsetOfHorizontal + 0]))
#define output_x2() (*reinterpret_cast<uint16_t *>(&next_output_run[OutputVertexOffsetOfHorizontal + 2]))
#define output_position_y() (*reinterpret_cast<uint16_t *>(&next_output_run[OutputVertexOffsetOfVertical + 0]))
#define output_tex_y() (*reinterpret_cast<uint16_t *>(&next_output_run[OutputVertexOffsetOfVertical + 2]))
Outputs::Display::ScanTarget::Scan::EndPoint CRT::end_point(uint16_t data_offset) {
Display::ScanTarget::Scan::EndPoint end_point;
#define source_input_position_y() (*reinterpret_cast<uint16_t *>(&next_run[SourceVertexOffsetOfInputStart + 2]))
#define source_output_position_x1() (*reinterpret_cast<uint16_t *>(&next_run[SourceVertexOffsetOfOutputStart + 0]))
#define source_output_position_x2() (*reinterpret_cast<uint16_t *>(&next_run[SourceVertexOffsetOfEnds + 2]))
#define source_phase() next_run[SourceVertexOffsetOfPhaseTimeAndAmplitude + 0]
#define source_amplitude() next_run[SourceVertexOffsetOfPhaseTimeAndAmplitude + 1]
end_point.x = uint16_t(horizontal_flywheel_->get_current_output_position());
end_point.y = uint16_t(vertical_flywheel_->get_current_output_position() / vertical_flywheel_output_divider_);
end_point.data_offset = data_offset;
void CRT::advance_cycles(unsigned int number_of_cycles, bool hsync_requested, bool vsync_requested, const Scan::Type type) {
std::unique_lock<std::mutex> output_lock = openGL_output_builder_.get_output_lock();
// TODO: this is a workaround for the limited precision that can be posted onwards;
// it'd be better to make time_multiplier_ an explicit modal and just not divide by it.
const auto lost_precision = cycles_since_horizontal_sync_ % time_multiplier_;
end_point.composite_angle = int16_t(((phase_numerator_ - lost_precision * colour_cycle_numerator_) << 6) / phase_denominator_) * (is_alernate_line_ ? -1 : 1);
end_point.cycles_since_end_of_horizontal_retrace = uint16_t(cycles_since_horizontal_sync_ / time_multiplier_);
return end_point;
}
void CRT::advance_cycles(int number_of_cycles, bool hsync_requested, bool vsync_requested, const Scan::Type type, int number_of_samples) {
number_of_cycles *= time_multiplier_;
bool is_output_run = ((type == Scan::Type::Level) || (type == Scan::Type::Data));
const bool is_output_run = ((type == Scan::Type::Level) || (type == Scan::Type::Data));
const auto total_cycles = number_of_cycles;
bool did_output = false;
while(number_of_cycles) {
unsigned int time_until_vertical_sync_event, time_until_horizontal_sync_event;
Flywheel::SyncEvent next_vertical_sync_event = get_next_vertical_sync_event(vsync_requested, number_of_cycles, &time_until_vertical_sync_event);
Flywheel::SyncEvent next_horizontal_sync_event = get_next_horizontal_sync_event(hsync_requested, time_until_vertical_sync_event, &time_until_horizontal_sync_event);
// Get time until next horizontal and vertical sync generator events.
int time_until_vertical_sync_event, time_until_horizontal_sync_event;
const Flywheel::SyncEvent next_vertical_sync_event = get_next_vertical_sync_event(vsync_requested, number_of_cycles, &time_until_vertical_sync_event);
const Flywheel::SyncEvent next_horizontal_sync_event = get_next_horizontal_sync_event(hsync_requested, time_until_vertical_sync_event, &time_until_horizontal_sync_event);
// get the next sync event and its timing; hsync request is instantaneous (being edge triggered) so
// set it to false for the next run through this loop (if any)
unsigned int next_run_length = std::min(time_until_vertical_sync_event, time_until_horizontal_sync_event);
phase_numerator_ += next_run_length * colour_cycle_numerator_;
phase_numerator_ %= phase_denominator_;
// Whichever event is scheduled to happen first is the one to advance to.
const int next_run_length = std::min(time_until_vertical_sync_event, time_until_horizontal_sync_event);
hsync_requested = false;
vsync_requested = false;
bool is_output_segment = ((is_output_run && next_run_length) && !horizontal_flywheel_->is_in_retrace() && !vertical_flywheel_->is_in_retrace());
uint8_t *next_run = nullptr;
if(is_output_segment && !openGL_output_builder_.composite_output_buffer_is_full()) {
bool did_retain_source_data = openGL_output_builder_.texture_builder.retain_latest();
if(did_retain_source_data) {
next_run = openGL_output_builder_.array_builder.get_input_storage(SourceVertexSize);
if(!next_run) {
openGL_output_builder_.texture_builder.discard_latest();
}
}
// Determine whether to output any data for this portion of the output; if so then grab somewhere to put it.
const bool is_output_segment = ((is_output_run && next_run_length) && !horizontal_flywheel_->is_in_retrace() && !vertical_flywheel_->is_in_retrace());
Outputs::Display::ScanTarget::Scan *const next_scan = is_output_segment ? scan_target_->begin_scan() : nullptr;
did_output |= is_output_segment;
// If outputting, store the start location and scan constants.
if(next_scan) {
next_scan->end_points[0] = end_point(uint16_t((total_cycles - number_of_cycles) * number_of_samples / total_cycles));
next_scan->composite_amplitude = colour_burst_amplitude_;
}
if(next_run) {
// output_y and texture locations will be written later; we won't necessarily know what they are
// outside of the locked region
source_output_position_x1() = static_cast<uint16_t>(horizontal_flywheel_->get_current_output_position());
source_phase() = colour_burst_phase_;
// TODO: determine what the PAL phase-shift machines actually do re: the swinging burst.
source_amplitude() = phase_alternates_ ? 128 - colour_burst_amplitude_ : 128 + colour_burst_amplitude_;
}
// decrement the number of cycles left to run for and increment the
// horizontal counter appropriately
// Advance time: that'll affect both the colour subcarrier position and the number of cycles left to run.
phase_numerator_ += next_run_length * colour_cycle_numerator_;
number_of_cycles -= next_run_length;
cycles_since_horizontal_sync_ += next_run_length;
// react to the incoming event...
// React to the incoming event.
horizontal_flywheel_->apply_event(next_run_length, (next_run_length == time_until_horizontal_sync_event) ? next_horizontal_sync_event : Flywheel::SyncEvent::None);
vertical_flywheel_->apply_event(next_run_length, (next_run_length == time_until_vertical_sync_event) ? next_vertical_sync_event : Flywheel::SyncEvent::None);
if(next_run) {
source_output_position_x2() = static_cast<uint16_t>(horizontal_flywheel_->get_current_output_position());
// End the scan if necessary.
if(next_scan) {
next_scan->end_points[1] = end_point(uint16_t((total_cycles - number_of_cycles) * number_of_samples / total_cycles));
scan_target_->end_scan();
}
// if this is horizontal retrace then advance the output line counter and bookend an output run
Flywheel::SyncEvent honoured_event = Flywheel::SyncEvent::None;
if(next_run_length == time_until_vertical_sync_event && next_vertical_sync_event != Flywheel::SyncEvent::None) honoured_event = next_vertical_sync_event;
if(next_run_length == time_until_horizontal_sync_event && next_horizontal_sync_event != Flywheel::SyncEvent::None) honoured_event = next_horizontal_sync_event;
bool needs_endpoint =
(honoured_event == Flywheel::SyncEvent::StartRetrace && is_writing_composite_run_) ||
(honoured_event == Flywheel::SyncEvent::EndRetrace && !horizontal_flywheel_->is_in_retrace() && !vertical_flywheel_->is_in_retrace());
// Announce horizontal retrace events.
if(next_run_length == time_until_horizontal_sync_event && next_horizontal_sync_event != Flywheel::SyncEvent::None) {
// Reset the cycles-since-sync counter if this is the end of retrace.
if(next_horizontal_sync_event == Flywheel::SyncEvent::EndRetrace) {
cycles_since_horizontal_sync_ = 0;
if(next_run_length == time_until_horizontal_sync_event && next_horizontal_sync_event == Flywheel::SyncEvent::StartRetrace) is_alernate_line_ ^= phase_alternates_;
// This is unnecessary, strictly speaking, but seeks to help ScanTargets fit as
// much as possible into a fixed range.
phase_numerator_ %= phase_denominator_;
if(!phase_numerator_) phase_numerator_ += phase_denominator_;
}
if(needs_endpoint) {
if(
!openGL_output_builder_.array_builder.is_full() &&
!openGL_output_builder_.composite_output_buffer_is_full()) {
// Announce event.
const auto event =
(next_horizontal_sync_event == Flywheel::SyncEvent::StartRetrace)
? Outputs::Display::ScanTarget::Event::BeginHorizontalRetrace : Outputs::Display::ScanTarget::Event::EndHorizontalRetrace;
scan_target_->announce(
event,
!(horizontal_flywheel_->is_in_retrace() || vertical_flywheel_->is_in_retrace()),
end_point(uint16_t((total_cycles - number_of_cycles) * number_of_samples / total_cycles)),
colour_burst_amplitude_);
if(!is_writing_composite_run_) {
output_run_.x1 = static_cast<uint16_t>(horizontal_flywheel_->get_current_output_position());
output_run_.y = static_cast<uint16_t>(vertical_flywheel_->get_current_output_position() / vertical_flywheel_output_divider_);
} else {
// Get and write all those previously unwritten output ys
const uint16_t output_y = openGL_output_builder_.get_composite_output_y();
// Construct the output run
uint8_t *next_output_run = openGL_output_builder_.array_builder.get_output_storage(OutputVertexSize);
if(next_output_run) {
output_x1() = output_run_.x1;
output_position_y() = output_run_.y;
output_tex_y() = output_y;
output_x2() = static_cast<uint16_t>(horizontal_flywheel_->get_current_output_position());
}
// TODO: below I've assumed a one-to-one correspondance with output runs and input data; that's
// obviously not completely sustainable. It's a latent bug.
openGL_output_builder_.array_builder.flush(
[=] (uint8_t *input_buffer, std::size_t input_size, uint8_t *output_buffer, std::size_t output_size) {
openGL_output_builder_.texture_builder.flush(
[=] (const std::vector<TextureBuilder::WriteArea> &write_areas, std::size_t number_of_write_areas) {
// assert(number_of_write_areas * SourceVertexSize == input_size);
if(number_of_write_areas * SourceVertexSize == input_size) {
for(std::size_t run = 0; run < number_of_write_areas; run++) {
*reinterpret_cast<uint16_t *>(&input_buffer[run * SourceVertexSize + SourceVertexOffsetOfInputStart + 0]) = write_areas[run].x;
*reinterpret_cast<uint16_t *>(&input_buffer[run * SourceVertexSize + SourceVertexOffsetOfInputStart + 2]) = write_areas[run].y;
*reinterpret_cast<uint16_t *>(&input_buffer[run * SourceVertexSize + SourceVertexOffsetOfEnds + 0]) = write_areas[run].x + write_areas[run].length;
}
}
});
for(std::size_t position = 0; position < input_size; position += SourceVertexSize) {
(*reinterpret_cast<uint16_t *>(&input_buffer[position + SourceVertexOffsetOfOutputStart + 2])) = output_y;
}
});
colour_burst_amplitude_ = 0;
}
is_writing_composite_run_ ^= true;
// If retrace is starting, update phase if required and mark no colour burst spotted yet.
if(next_horizontal_sync_event == Flywheel::SyncEvent::StartRetrace) {
is_alernate_line_ ^= phase_alternates_;
colour_burst_amplitude_ = 0;
}
}
if(next_run_length == time_until_horizontal_sync_event && next_horizontal_sync_event == Flywheel::SyncEvent::StartRetrace) {
openGL_output_builder_.increment_composite_output_y();
// Also announce vertical retrace events.
if(next_run_length == time_until_vertical_sync_event && next_vertical_sync_event != Flywheel::SyncEvent::None) {
const auto event =
(next_vertical_sync_event == Flywheel::SyncEvent::StartRetrace)
? Outputs::Display::ScanTarget::Event::BeginVerticalRetrace : Outputs::Display::ScanTarget::Event::EndVerticalRetrace;
scan_target_->announce(
event,
!(horizontal_flywheel_->is_in_retrace() || vertical_flywheel_->is_in_retrace()),
end_point(uint16_t((total_cycles - number_of_cycles) * number_of_samples / total_cycles)),
colour_burst_amplitude_);
}
// if this is vertical retrace then adcance a field
@ -260,39 +272,34 @@ void CRT::advance_cycles(unsigned int number_of_cycles, bool hsync_requested, bo
if(delegate_) {
frames_since_last_delegate_call_++;
if(frames_since_last_delegate_call_ == 20) {
output_lock.unlock();
delegate_->crt_did_end_batch_of_frames(this, frames_since_last_delegate_call_, vertical_flywheel_->get_and_reset_number_of_surprises());
output_lock.lock();
frames_since_last_delegate_call_ = 0;
}
}
}
}
if(did_output) {
scan_target_->submit();
}
}
#undef output_x1
#undef output_x2
#undef output_position_y
#undef output_tex_y
#undef source_input_position_y
#undef source_output_position_x1
#undef source_output_position_x2
#undef source_phase
#undef source_amplitude
// MARK: - stream feeding methods
void CRT::output_scan(const Scan *const scan) {
// simplified colour burst logic: if it's within the back porch we'll take it
// Simplified colour burst logic: if it's within the back porch we'll take it.
if(scan->type == Scan::Type::ColourBurst) {
if(!colour_burst_amplitude_ && horizontal_flywheel_->get_current_time() < (horizontal_flywheel_->get_standard_period() * 12) >> 6) {
unsigned int position_phase = (horizontal_flywheel_->get_current_time() * colour_cycle_numerator_ * 256) / phase_denominator_;
colour_burst_phase_ = (position_phase + scan->phase) & 255;
colour_burst_amplitude_ = scan->amplitude;
// Load phase_numerator_ as a fixed-point quantity in the range [0, 255].
phase_numerator_ = scan->phase;
if(colour_burst_phase_adjustment_ != 0xff)
colour_burst_phase_ = (colour_burst_phase_ & ~63) + colour_burst_phase_adjustment_;
phase_numerator_ = (phase_numerator_ & ~63) + colour_burst_phase_adjustment_;
// Multiply the phase_numerator_ up to be to the proper scale.
phase_numerator_ = (phase_numerator_ * phase_denominator_) >> 8;
// Crib the colour burst amplitude.
colour_burst_amplitude_ = scan->amplitude;
}
}
// TODO: inspect raw data for potential colour burst if required; the DPLL and some zero crossing logic
@ -303,18 +310,18 @@ void CRT::output_scan(const Scan *const scan) {
const bool is_leading_edge = (!is_receiving_sync_ && this_is_sync);
is_receiving_sync_ = this_is_sync;
// horizontal sync is recognised on any leading edge that is not 'near' the expected vertical sync;
// Horizontal sync is recognised on any leading edge that is not 'near' the expected vertical sync;
// the second limb is to avoid slightly horizontal sync shifting from the common pattern of
// equalisation pulses as the inverse of ordinary horizontal sync
// equalisation pulses as the inverse of ordinary horizontal sync.
bool hsync_requested = is_leading_edge && !vertical_flywheel_->is_near_expected_sync();
if(this_is_sync) {
// if this is sync then either begin or continue a sync accumulation phase
// If this is sync then either begin or continue a sync accumulation phase.
is_accumulating_sync_ = true;
cycles_since_sync_ = 0;
} else {
// if this is not sync then check how long it has been since sync. If it's more than
// half a line then end sync accumulation and zero out the accumulating count
// If this is not sync then check how long it has been since sync. If it's more than
// half a line then end sync accumulation and zero out the accumulating count.
cycles_since_sync_ += scan->number_of_cycles;
if(cycles_since_sync_ > (cycles_per_line_ >> 2)) {
cycles_of_sync_ = 0;
@ -323,19 +330,19 @@ void CRT::output_scan(const Scan *const scan) {
}
}
unsigned int number_of_cycles = scan->number_of_cycles;
int number_of_cycles = scan->number_of_cycles;
bool vsync_requested = false;
// if sync is being accumulated then accumulate it; if it crosses the vertical sync threshold then
// divide this line at the crossing point and indicate vertical sync there
// If sync is being accumulated then accumulate it; if it crosses the vertical sync threshold then
// divide this line at the crossing point and indicate vertical sync there.
if(is_accumulating_sync_ && !is_refusing_sync_) {
cycles_of_sync_ += scan->number_of_cycles;
if(this_is_sync && cycles_of_sync_ >= sync_capacitor_charge_threshold_) {
unsigned int overshoot = std::min(cycles_of_sync_ - sync_capacitor_charge_threshold_, number_of_cycles);
const int overshoot = std::min(cycles_of_sync_ - sync_capacitor_charge_threshold_, number_of_cycles);
if(overshoot) {
number_of_cycles -= overshoot;
advance_cycles(number_of_cycles, hsync_requested, false, scan->type);
advance_cycles(number_of_cycles, hsync_requested, false, scan->type, 0);
hsync_requested = false;
number_of_cycles = overshoot;
}
@ -345,35 +352,36 @@ void CRT::output_scan(const Scan *const scan) {
}
}
advance_cycles(number_of_cycles, hsync_requested, vsync_requested, scan->type);
advance_cycles(number_of_cycles, hsync_requested, vsync_requested, scan->type, scan->number_of_samples);
}
/*
These all merely channel into advance_cycles, supplying appropriate arguments
*/
void CRT::output_sync(unsigned int number_of_cycles) {
void CRT::output_sync(int number_of_cycles) {
Scan scan;
scan.type = Scan::Type::Sync;
scan.number_of_cycles = number_of_cycles;
output_scan(&scan);
}
void CRT::output_blank(unsigned int number_of_cycles) {
void CRT::output_blank(int number_of_cycles) {
Scan scan;
scan.type = Scan::Type::Blank;
scan.number_of_cycles = number_of_cycles;
output_scan(&scan);
}
void CRT::output_level(unsigned int number_of_cycles) {
openGL_output_builder_.texture_builder.reduce_previous_allocation_to(1);
void CRT::output_level(int number_of_cycles) {
scan_target_->end_data(1);
Scan scan;
scan.type = Scan::Type::Level;
scan.number_of_cycles = number_of_cycles;
scan.number_of_samples = 1;
output_scan(&scan);
}
void CRT::output_colour_burst(unsigned int number_of_cycles, uint8_t phase, uint8_t amplitude) {
void CRT::output_colour_burst(int number_of_cycles, uint8_t phase, uint8_t amplitude) {
Scan scan;
scan.type = Scan::Type::ColourBurst;
scan.number_of_cycles = number_of_cycles;
@ -382,24 +390,26 @@ void CRT::output_colour_burst(unsigned int number_of_cycles, uint8_t phase, uint
output_scan(&scan);
}
void CRT::output_default_colour_burst(unsigned int number_of_cycles) {
output_colour_burst(number_of_cycles, static_cast<uint8_t>((phase_numerator_ * 256) / phase_denominator_));
void CRT::output_default_colour_burst(int number_of_cycles, uint8_t amplitude) {
// TODO: avoid applying a rounding error here?
output_colour_burst(number_of_cycles, static_cast<uint8_t>((phase_numerator_ * 256) / phase_denominator_), amplitude);
}
void CRT::set_immediate_default_phase(float phase) {
phase = fmodf(phase, 1.0f);
phase_numerator_ = static_cast<unsigned int>(phase * static_cast<float>(phase_denominator_));
phase_numerator_ = static_cast<int>(phase * static_cast<float>(phase_denominator_));
}
void CRT::output_data(unsigned int number_of_cycles, unsigned int number_of_samples) {
openGL_output_builder_.texture_builder.reduce_previous_allocation_to(number_of_samples);
void CRT::output_data(int number_of_cycles, size_t number_of_samples) {
scan_target_->end_data(number_of_samples);
Scan scan;
scan.type = Scan::Type::Data;
scan.number_of_cycles = number_of_cycles;
scan.number_of_samples = int(number_of_samples);
output_scan(&scan);
}
Outputs::CRT::Rect CRT::get_rect_for_area(int first_line_after_sync, int number_of_lines, int first_cycle_after_sync, int number_of_cycles, float aspect_ratio) {
Outputs::Display::Rect CRT::get_rect_for_area(int first_line_after_sync, int number_of_lines, int first_cycle_after_sync, int number_of_cycles, float aspect_ratio) {
first_cycle_after_sync *= time_multiplier_;
number_of_cycles *= time_multiplier_;
@ -407,34 +417,34 @@ Outputs::CRT::Rect CRT::get_rect_for_area(int first_line_after_sync, int number_
number_of_lines += 4;
// determine prima facie x extent
unsigned int horizontal_period = horizontal_flywheel_->get_standard_period();
unsigned int horizontal_scan_period = horizontal_flywheel_->get_scan_period();
unsigned int horizontal_retrace_period = horizontal_period - horizontal_scan_period;
const int horizontal_period = horizontal_flywheel_->get_standard_period();
const int horizontal_scan_period = horizontal_flywheel_->get_scan_period();
const int horizontal_retrace_period = horizontal_period - horizontal_scan_period;
// make sure that the requested range is visible
if(static_cast<unsigned int>(first_cycle_after_sync) < horizontal_retrace_period) first_cycle_after_sync = static_cast<int>(horizontal_retrace_period);
if(static_cast<unsigned int>(first_cycle_after_sync + number_of_cycles) > horizontal_scan_period) number_of_cycles = static_cast<int>(horizontal_scan_period - static_cast<unsigned int>(first_cycle_after_sync));
if(static_cast<int>(first_cycle_after_sync) < horizontal_retrace_period) first_cycle_after_sync = static_cast<int>(horizontal_retrace_period);
if(static_cast<int>(first_cycle_after_sync + number_of_cycles) > horizontal_scan_period) number_of_cycles = static_cast<int>(horizontal_scan_period - static_cast<int>(first_cycle_after_sync));
float start_x = static_cast<float>(static_cast<unsigned int>(first_cycle_after_sync) - horizontal_retrace_period) / static_cast<float>(horizontal_scan_period);
float start_x = static_cast<float>(static_cast<int>(first_cycle_after_sync) - horizontal_retrace_period) / static_cast<float>(horizontal_scan_period);
float width = static_cast<float>(number_of_cycles) / static_cast<float>(horizontal_scan_period);
// determine prima facie y extent
unsigned int vertical_period = vertical_flywheel_->get_standard_period();
unsigned int vertical_scan_period = vertical_flywheel_->get_scan_period();
unsigned int vertical_retrace_period = vertical_period - vertical_scan_period;
const int vertical_period = vertical_flywheel_->get_standard_period();
const int vertical_scan_period = vertical_flywheel_->get_scan_period();
const int vertical_retrace_period = vertical_period - vertical_scan_period;
// make sure that the requested range is visible
// if(static_cast<unsigned int>(first_line_after_sync) * horizontal_period < vertical_retrace_period)
// if(static_cast<int>(first_line_after_sync) * horizontal_period < vertical_retrace_period)
// first_line_after_sync = (vertical_retrace_period + horizontal_period - 1) / horizontal_period;
// if((first_line_after_sync + number_of_lines) * horizontal_period > vertical_scan_period)
// number_of_lines = static_cast<int>(horizontal_scan_period - static_cast<unsigned int>(first_cycle_after_sync));
// number_of_lines = static_cast<int>(horizontal_scan_period - static_cast<int>(first_cycle_after_sync));
float start_y = static_cast<float>((static_cast<unsigned int>(first_line_after_sync) * horizontal_period) - vertical_retrace_period) / static_cast<float>(vertical_scan_period);
float height = static_cast<float>(static_cast<unsigned int>(number_of_lines) * horizontal_period) / vertical_scan_period;
float start_y = static_cast<float>((static_cast<int>(first_line_after_sync) * horizontal_period) - vertical_retrace_period) / static_cast<float>(vertical_scan_period);
float height = static_cast<float>(static_cast<int>(number_of_lines) * horizontal_period) / vertical_scan_period;
// adjust to ensure aspect ratio is correct
float adjusted_aspect_ratio = (3.0f*aspect_ratio / 4.0f);
float ideal_width = height * adjusted_aspect_ratio;
const float adjusted_aspect_ratio = (3.0f*aspect_ratio / 4.0f);
const float ideal_width = height * adjusted_aspect_ratio;
if(ideal_width > width) {
start_x -= (ideal_width - width) * 0.5f;
width = ideal_width;
@ -444,5 +454,5 @@ Outputs::CRT::Rect CRT::get_rect_for_area(int first_line_after_sync, int number_
height = ideal_height;
}
return Rect(start_x, start_y, width, height);
return Outputs::Display::Rect(start_x, start_y, width, height);
}

View File

@ -10,12 +10,10 @@
#define CRT_hpp
#include <cstdint>
#include <memory>
#include "CRTTypes.hpp"
#include "../ScanTarget.hpp"
#include "Internals/Flywheel.hpp"
#include "Internals/CRTOpenGL.hpp"
#include "Internals/ArrayBuilder.hpp"
#include "Internals/TextureBuilder.hpp"
namespace Outputs {
namespace CRT {
@ -24,81 +22,66 @@ class CRT;
class Delegate {
public:
virtual void crt_did_end_batch_of_frames(CRT *crt, unsigned int number_of_frames, unsigned int number_of_unexpected_vertical_syncs) = 0;
virtual void crt_did_end_batch_of_frames(CRT *crt, int number_of_frames, int number_of_unexpected_vertical_syncs) = 0;
};
/*! Models a class 2d analogue output device, accepting a serial stream of data including syncs
and generating the proper set of output spans. Attempts to act and react exactly as a real
TV would have to things like irregular or off-spec sync, and includes logic properly to track
colour phase for colour composite video.
*/
class CRT {
private:
CRT(unsigned int common_output_divisor, unsigned int buffer_depth);
// The incoming clock lengths will be multiplied by @c time_multiplier_; this increases
// precision across the line.
int time_multiplier_ = 1;
// the incoming clock lengths will be multiplied by something to give at least 1000
// sample points per line
unsigned int time_multiplier_ = 1;
const unsigned int common_output_divisor_ = 1;
// the two flywheels regulating scanning
// Two flywheels regulate scanning; the vertical will have a range much greater than the horizontal;
// the output divider is what that'll need to be divided by to reduce it into a 16-bit range as
// posted on to the scan target.
std::unique_ptr<Flywheel> horizontal_flywheel_, vertical_flywheel_;
uint16_t vertical_flywheel_output_divider_ = 1;
int vertical_flywheel_output_divider_ = 1;
int cycles_since_horizontal_sync_ = 0;
Display::ScanTarget::Scan::EndPoint end_point(uint16_t data_offset);
struct Scan {
enum Type {
Sync, Level, Data, Blank, ColourBurst
} type;
unsigned int number_of_cycles;
union {
struct {
uint8_t phase, amplitude;
};
};
} type = Scan::Blank;
int number_of_cycles = 0, number_of_samples = 0;
uint8_t phase = 0, amplitude = 0;
};
void output_scan(const Scan *scan);
uint8_t colour_burst_phase_ = 0, colour_burst_amplitude_ = 30, colour_burst_phase_adjustment_ = 0;
int16_t colour_burst_angle_ = 0;
uint8_t colour_burst_amplitude_ = 30;
int colour_burst_phase_adjustment_ = 0xff;
bool is_writing_composite_run_ = false;
unsigned int phase_denominator_ = 1, phase_numerator_ = 1, colour_cycle_numerator_ = 1;
int64_t phase_denominator_ = 1;
int64_t phase_numerator_ = 0;
int64_t colour_cycle_numerator_ = 1;
bool is_alernate_line_ = false, phase_alternates_ = false;
// the outer entry point for dispatching output_sync, output_blank, output_level and output_data
void advance_cycles(unsigned int number_of_cycles, bool hsync_requested, bool vsync_requested, const Scan::Type type);
void advance_cycles(int number_of_cycles, bool hsync_requested, bool vsync_requested, const Scan::Type type, int number_of_samples);
Flywheel::SyncEvent get_next_vertical_sync_event(bool vsync_is_requested, int cycles_to_run_for, int *cycles_advanced);
Flywheel::SyncEvent get_next_horizontal_sync_event(bool hsync_is_requested, int cycles_to_run_for, int *cycles_advanced);
// the inner entry point that determines whether and when the next sync event will occur within
// the current output window
Flywheel::SyncEvent get_next_vertical_sync_event(bool vsync_is_requested, unsigned int cycles_to_run_for, unsigned int *cycles_advanced);
Flywheel::SyncEvent get_next_horizontal_sync_event(bool hsync_is_requested, unsigned int cycles_to_run_for, unsigned int *cycles_advanced);
// OpenGL state
OpenGLOutputBuilder openGL_output_builder_;
// temporary storage used during the construction of output runs
struct {
uint16_t x1, y;
} output_run_;
// the delegate
Delegate *delegate_ = nullptr;
unsigned int frames_since_last_delegate_call_ = 0;
int frames_since_last_delegate_call_ = 0;
// queued tasks for the OpenGL queue; performed before the next draw
std::mutex function_mutex_;
std::vector<std::function<void(void)>> enqueued_openGL_functions_;
inline void enqueue_openGL_function(const std::function<void(void)> &function) {
std::lock_guard<std::mutex> function_guard(function_mutex_);
enqueued_openGL_functions_.push_back(function);
}
bool is_receiving_sync_ = false; // @c true if the CRT is currently receiving sync (i.e. this is for edge triggering of horizontal sync); @c false otherwise.
bool is_accumulating_sync_ = false; // @c true if a sync level has triggered the suspicion that a vertical sync might be in progress; @c false otherwise.
bool is_refusing_sync_ = false; // @c true once a vertical sync has been detected, until a prolonged period of non-sync has ended suspicion of an ongoing vertical sync.
int sync_capacitor_charge_threshold_ = 0; // Charges up during times of sync and depletes otherwise; needs to hit a required threshold to trigger a vertical sync.
int cycles_of_sync_ = 0; // The number of cycles since the potential vertical sync began.
int cycles_since_sync_ = 0; // The number of cycles since last in sync, for defeating the possibility of this being a vertical sync.
// sync counter, for determining vertical sync
bool is_receiving_sync_ = false; // true if the CRT is currently receiving sync (i.e. this is for edge triggering of horizontal sync)
bool is_accumulating_sync_ = false; // true if a sync level has triggered the suspicion that a vertical sync might be in progress
bool is_refusing_sync_ = false; // true once a vertical sync has been detected, until a prolonged period of non-sync has ended suspicion of an ongoing vertical sync
unsigned int sync_capacitor_charge_threshold_ = 0; // this charges up during times of sync and depletes otherwise; needs to hit a required threshold to trigger a vertical sync
unsigned int cycles_of_sync_ = 0; // the number of cycles since the potential vertical sync began
unsigned int cycles_since_sync_ = 0; // the number of cycles since last in sync, for defeating the possibility of this being a vertical sync
int cycles_per_line_ = 1;
unsigned int cycles_per_line_ = 1;
float input_gamma_ = 1.0f, output_gamma_ = 1.0f;
void update_gamma();
Outputs::Display::ScanTarget *scan_target_ = &Outputs::Display::NullScanTarget::singleton;
Outputs::Display::ScanTarget::Modals scan_target_modals_;
static const uint8_t DefaultAmplitude = 80;
public:
/*! Constructs the CRT with a specified clock rate, height and colour subcarrier frequency.
@ -108,10 +91,8 @@ class CRT {
@param cycles_per_line The clock rate at which this CRT will be driven, specified as the number
of cycles expected to take up one whole scanline of the display.
@param common_output_divisor The greatest a priori common divisor of all cycle counts that will be
supplied to @c output_sync, @c output_data, etc; supply 1 if no greater divisor is known. For many
machines output will run at a fixed multiple of the clock rate; knowing this divisor can improve
internal precision.
@param clocks_per_pixel_greatest_common_divisor The GCD of all potential lengths of a pixel
in terms of the clock rate given as @c cycles_per_line.
@param height_of_display The number of lines that nominally form one field of the display, rounded
up to the next whole integer.
@ -124,76 +105,79 @@ class CRT {
@param vertical_sync_half_lines The expected length of vertical synchronisation (equalisation pulses aside),
in multiples of half a line.
@param buffer_depth The depth per pixel of source data buffers to create for this machine. Machines
may provide per-clock-cycle data in the depth that they consider convenient, supplying a sampling
function to convert between their data format and either a composite or RGB signal, allowing that
work to be offloaded onto the GPU and allowing the output signal to be sampled at a rate appropriate
to the display size.
@see @c set_rgb_sampling_function , @c set_composite_sampling_function
@param data_type The format that the caller will use for input data.
*/
CRT(unsigned int cycles_per_line,
unsigned int common_output_divisor,
unsigned int height_of_display,
ColourSpace colour_space,
unsigned int colour_cycle_numerator, unsigned int colour_cycle_denominator,
unsigned int vertical_sync_half_lines,
CRT(int cycles_per_line,
int clocks_per_pixel_greatest_common_divisor,
int height_of_display,
Outputs::Display::ColourSpace colour_space,
int colour_cycle_numerator,
int colour_cycle_denominator,
int vertical_sync_half_lines,
bool should_alternate,
unsigned int buffer_depth);
Outputs::Display::InputDataType data_type);
/*! Constructs the CRT with the specified clock rate, with the display height and colour
subcarrier frequency dictated by a standard display type and with the requested number of
buffers, each with the requested number of bytes per pixel.
Exactly identical to calling the designated constructor with colour subcarrier information
/*! Exactly identical to calling the designated constructor with colour subcarrier information
looked up by display type.
*/
CRT(unsigned int cycles_per_line,
unsigned int common_output_divisor,
DisplayType displayType,
unsigned int buffer_depth);
CRT(int cycles_per_line,
int minimum_cycles_per_pixel,
Outputs::Display::Type display_type,
Outputs::Display::InputDataType data_type);
/*! Resets the CRT with new timing information. The CRT then continues as though the new timing had
been provided at construction. */
void set_new_timing(unsigned int cycles_per_line, unsigned int height_of_display, ColourSpace colour_space, unsigned int colour_cycle_numerator, unsigned int colour_cycle_denominator, unsigned int vertical_sync_half_lines, bool should_alternate);
void set_new_timing(
int cycles_per_line,
int height_of_display,
Outputs::Display::ColourSpace colour_space,
int colour_cycle_numerator,
int colour_cycle_denominator,
int vertical_sync_half_lines,
bool should_alternate);
/*! Resets the CRT with new timing information derived from a new display type. The CRT then continues
as though the new timing had been provided at construction. */
void set_new_display_type(unsigned int cycles_per_line, DisplayType displayType);
void set_new_display_type(
int cycles_per_line,
Outputs::Display::Type display_type);
/*! Changes the type of data being supplied as input.
*/
void set_new_data_type(Outputs::Display::InputDataType data_type);
/*! Output at the sync level.
@param number_of_cycles The amount of time to putput sync for.
*/
void output_sync(unsigned int number_of_cycles);
void output_sync(int number_of_cycles);
/*! Output at the blanking level.
@param number_of_cycles The amount of time to putput the blanking level for.
*/
void output_blank(unsigned int number_of_cycles);
void output_blank(int number_of_cycles);
/*! Outputs the first written to the most-recently created run of data repeatedly for a prolonged period.
@param number_of_cycles The number of cycles to repeat the output for.
*/
void output_level(unsigned int number_of_cycles);
void output_level(int number_of_cycles);
/*! Declares that the caller has created a run of data via @c allocate_write_area and @c get_write_target_for_buffer
that is at least @c number_of_samples long, and that the first @c number_of_samples should be spread
over @c number_of_cycles.
/*! Declares that the caller has created a run of data via @c begin_data that is at least @c number_of_samples
long, and that the first @c number_of_samples should be spread over @c number_of_cycles.
@param number_of_cycles The amount of data to output.
@param number_of_samples The number of samples of input data to output.
@see @c allocate_write_area , @c get_write_target_for_buffer
@see @c begin_data
*/
void output_data(unsigned int number_of_cycles, unsigned int number_of_samples);
void output_data(int number_of_cycles, size_t number_of_samples);
/*! A shorthand form for output_data that assumes the number of cycles to output for is the same as the number of samples. */
void output_data(unsigned int number_of_cycles) {
output_data(number_of_cycles, number_of_cycles);
void output_data(int number_of_cycles) {
output_data(number_of_cycles, size_t(number_of_cycles));
}
/*! Outputs a colour burst.
@ -203,16 +187,16 @@ class CRT {
@param phase The initial phase of the colour burst in a measuring system with 256 units
per circle, e.g. 0 = 0 degrees, 128 = 180 degrees, 256 = 360 degree.
@param amplitude The amplitude of the colour burst in 1/256ths of the amplitude of the
@param amplitude The amplitude of the colour burst in 1/255ths of the amplitude of the
positive portion of the wave.
*/
void output_colour_burst(unsigned int number_of_cycles, uint8_t phase, uint8_t amplitude = 102);
void output_colour_burst(int number_of_cycles, uint8_t phase, uint8_t amplitude = DefaultAmplitude);
/*! Outputs a colour burst exactly in phase with CRT expectations using the idiomatic amplitude.
@param number_of_cycles The length of the colour burst;
*/
void output_default_colour_burst(unsigned int number_of_cycles);
void output_default_colour_burst(int number_of_cycles, uint8_t amplitude = DefaultAmplitude);
/*! Sets the current phase of the colour subcarrier used by output_default_colour_burst.
@ -232,64 +216,13 @@ class CRT {
@param required_length The number of samples to allocate.
@returns A pointer to the allocated area if room is available; @c nullptr otherwise.
*/
inline uint8_t *allocate_write_area(std::size_t required_length, std::size_t required_alignment = 1) {
std::unique_lock<std::mutex> output_lock = openGL_output_builder_.get_output_lock();
return openGL_output_builder_.texture_builder.allocate_write_area(required_length, required_alignment);
}
/*! Causes appropriate OpenGL or OpenGL ES calls to be issued in order to draw the current CRT state.
The caller is responsible for ensuring that a valid OpenGL context exists for the duration of this call.
*/
inline void draw_frame(unsigned int output_width, unsigned int output_height, bool only_if_dirty) {
{
std::lock_guard<std::mutex> function_guard(function_mutex_);
for(std::function<void(void)> function : enqueued_openGL_functions_) {
function();
}
enqueued_openGL_functions_.clear();
}
openGL_output_builder_.draw_frame(output_width, output_height, only_if_dirty);
}
/*! Sets the OpenGL framebuffer to which output is drawn. */
inline void set_target_framebuffer(GLint framebuffer) {
enqueue_openGL_function( [framebuffer, this] {
openGL_output_builder_.set_target_framebuffer(framebuffer);
});
inline uint8_t *begin_data(std::size_t required_length, std::size_t required_alignment = 1) {
return scan_target_->begin_data(required_length, required_alignment);
}
/*! Sets the gamma exponent for the simulated screen. */
void set_input_gamma(float gamma);
/*! Sets the gamma exponent for the real, tangible screen on which content will be drawn. */
void set_output_gamma(float gamma);
/*! Tells the CRT that the next call to draw_frame will occur on a different OpenGL context than
the previous.
@param should_delete_resources If @c true then all resources, textures, vertex arrays, etc,
currently held by the CRT will be deleted now via calls to glDeleteTexture and equivalent. If
@c false then the references are simply marked as invalid.
*/
inline void set_openGL_context_will_change(bool should_delete_resources) {
enqueue_openGL_function([should_delete_resources, this] {
openGL_output_builder_.set_openGL_context_will_change(should_delete_resources);
});
}
/*! Sets a function that will map from whatever data the machine provided to a composite signal.
@param shader A GLSL fragment including a function with the signature
`float composite_sample(usampler2D texID, vec2 coordinate, float phase, float amplitude)`
that evaluates to the composite signal level as a function of a source buffer, sampling location, colour
carrier phase and amplitude.
*/
inline void set_composite_sampling_function(const std::string &shader) {
enqueue_openGL_function([shader, this] {
openGL_output_builder_.set_composite_sampling_function(shader);
});
}
enum CompositeSourceType {
/// The composite function provides continuous output.
Continuous,
@ -310,58 +243,36 @@ class CRT {
*/
void set_composite_function_type(CompositeSourceType type, float offset_of_first_sample = 0.0f);
/*! Sets a function that will map from whatever data the machine provided to an s-video signal.
/*! Nominates a section of the display to crop to for output. */
void set_visible_area(Outputs::Display::Rect visible_area);
If the output mode is composite then a default mapping from RGB to the display's
output mode will be applied.
@param shader A GLSL fragment including a function with the signature
`vec2 svideo_sample(usampler2D texID, vec2 coordinate, float phase, float amplitude)`
that evaluates to the s-video signal level, luminance as the first component and chrominance
as the second, as a function of a source buffer, sampling location and colour
carrier phase; amplitude is supplied for its sign.
*/
inline void set_svideo_sampling_function(const std::string &shader) {
enqueue_openGL_function([shader, this] {
openGL_output_builder_.set_svideo_sampling_function(shader);
});
}
/*! Sets a function that will map from whatever data the machine provided to an RGB signal.
If the output mode is composite or svideo then a default mapping from RGB to the display's
output mode will be applied.
@param shader A GLSL fragent including a function with the signature
`vec3 rgb_sample(usampler2D sampler, vec2 coordinate)` that evaluates to an RGB colour
as a function of:
* `usampler2D sampler` representing the source buffer; and
* `vec2 coordinate` representing the source buffer location to sample from in the range [0, 1).
*/
inline void set_rgb_sampling_function(const std::string &shader) {
enqueue_openGL_function([shader, this] {
openGL_output_builder_.set_rgb_sampling_function(shader);
});
}
inline void set_video_signal(VideoSignal video_signal) {
enqueue_openGL_function([video_signal, this] {
openGL_output_builder_.set_video_signal(video_signal);
});
}
inline void set_visible_area(Rect visible_area) {
enqueue_openGL_function([visible_area, this] {
openGL_output_builder_.set_visible_area(visible_area);
});
}
Rect get_rect_for_area(int first_line_after_sync, int number_of_lines, int first_cycle_after_sync, int number_of_cycles, float aspect_ratio);
/*! @returns The rectangle describing a subset of the display, allowing for sync periods. */
Outputs::Display::Rect get_rect_for_area(
int first_line_after_sync,
int number_of_lines,
int first_cycle_after_sync,
int number_of_cycles,
float aspect_ratio);
/*! Sets the CRT delegate; set to @c nullptr if no delegate is desired. */
inline void set_delegate(Delegate *delegate) {
delegate_ = delegate;
}
/*! Sets the scan target for CRT output. */
void set_scan_target(Outputs::Display::ScanTarget *);
/*! Sets the display type that will be nominated to the scan target. */
void set_display_type(Outputs::Display::DisplayType);
/*! Sets the offset to apply to phase when using the PhaseLinkedLuminance8 input data type. */
void set_phase_linked_luminance_offset(float);
/*! Sets the input data type. */
void set_input_data_type(Outputs::Display::InputDataType);
/*! Sets the output brightness. */
void set_brightness(float);
};
}

View File

@ -1,48 +0,0 @@
//
// CRTTypes.hpp
// Clock Signal
//
// Created by Thomas Harte on 08/03/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#ifndef CRTTypes_h
#define CRTTypes_h
namespace Outputs {
namespace CRT {
struct Rect {
struct {
float x, y;
} origin;
struct {
float width, height;
} size;
Rect() {}
Rect(float x, float y, float width, float height) :
origin({x, y}), size({width, height}) {}
};
enum class DisplayType {
PAL50,
NTSC60
};
enum class ColourSpace {
YIQ,
YUV
};
enum class VideoSignal {
RGB,
SVideo,
Composite
};
}
}
#endif /* CRTTypes_h */

View File

@ -1,149 +0,0 @@
//
// ArrayBuilder.cpp
// Clock Signal
//
// Created by Thomas Harte on 17/11/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#include "ArrayBuilder.hpp"
#include <cstring>
using namespace Outputs::CRT;
ArrayBuilder::ArrayBuilder(std::size_t input_size, std::size_t output_size) :
output_(output_size, nullptr),
input_(input_size, nullptr) {}
ArrayBuilder::ArrayBuilder(std::size_t input_size, std::size_t output_size, std::function<void(bool is_input, uint8_t *, std::size_t)> submission_function) :
output_(output_size, submission_function),
input_(input_size, submission_function) {}
bool ArrayBuilder::is_full() {
bool was_full;
was_full = is_full_;
return was_full;
}
uint8_t *ArrayBuilder::get_input_storage(std::size_t size) {
return get_storage(size, input_);
}
uint8_t *ArrayBuilder::get_output_storage(std::size_t size) {
return get_storage(size, output_);
}
void ArrayBuilder::flush(const std::function<void(uint8_t *input, std::size_t input_size, uint8_t *output, std::size_t output_size)> &function) {
if(!is_full_) {
std::size_t input_size = 0, output_size = 0;
uint8_t *input = input_.get_unflushed(input_size);
uint8_t *output = output_.get_unflushed(output_size);
function(input, input_size, output, output_size);
input_.flush();
output_.flush();
}
}
void ArrayBuilder::bind_input() {
input_.bind();
}
void ArrayBuilder::bind_output() {
output_.bind();
}
ArrayBuilder::Submission ArrayBuilder::submit() {
ArrayBuilder::Submission submission;
submission.input_size = input_.submit(true);
submission.output_size = output_.submit(false);
if(is_full_) {
is_full_ = false;
input_.reset();
output_.reset();
}
return submission;
}
ArrayBuilder::Buffer::Buffer(std::size_t size, std::function<void(bool is_input, uint8_t *, std::size_t)> submission_function) :
submission_function_(submission_function) {
if(!submission_function_) {
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)size, NULL, GL_STREAM_DRAW);
}
data.resize(size);
}
ArrayBuilder::Buffer::~Buffer() {
if(!submission_function_)
glDeleteBuffers(1, &buffer);
}
uint8_t *ArrayBuilder::get_storage(std::size_t size, Buffer &buffer) {
uint8_t *pointer = buffer.get_storage(size);
if(!pointer) is_full_ = true;
return pointer;
}
uint8_t *ArrayBuilder::Buffer::get_storage(std::size_t size) {
if(is_full || allocated_data + size > data.size()) {
is_full = true;
return nullptr;
}
uint8_t *pointer = &data[allocated_data];
allocated_data += size;
return pointer;
}
uint8_t *ArrayBuilder::Buffer::get_unflushed(std::size_t &size) {
if(is_full) {
return nullptr;
}
size = allocated_data - flushed_data;
return &data[flushed_data];
}
void ArrayBuilder::Buffer::flush() {
if(submitted_data) {
memmove(data.data(), &data[submitted_data], allocated_data - submitted_data);
allocated_data -= submitted_data;
flushed_data -= submitted_data;
submitted_data = 0;
}
flushed_data = allocated_data;
}
std::size_t ArrayBuilder::Buffer::submit(bool is_input) {
std::size_t length = flushed_data;
if(submission_function_) {
submission_function_(is_input, data.data(), length);
} else {
glBindBuffer(GL_ARRAY_BUFFER, buffer);
uint8_t *destination = static_cast<uint8_t *>(glMapBufferRange(GL_ARRAY_BUFFER, 0, (GLsizeiptr)length, GL_MAP_WRITE_BIT | GL_MAP_UNSYNCHRONIZED_BIT | GL_MAP_FLUSH_EXPLICIT_BIT));
if(!glGetError() && destination) {
std::memcpy(destination, data.data(), length);
glFlushMappedBufferRange(GL_ARRAY_BUFFER, 0, (GLsizeiptr)length);
glUnmapBuffer(GL_ARRAY_BUFFER);
} else {
glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)length, data.data(), GL_STREAM_DRAW);
}
}
submitted_data = flushed_data;
return length;
}
void ArrayBuilder::Buffer::bind() {
glBindBuffer(GL_ARRAY_BUFFER, buffer);
}
void ArrayBuilder::Buffer::reset() {
is_full = false;
allocated_data = 0;
flushed_data = 0;
submitted_data = 0;
}

View File

@ -1,101 +0,0 @@
//
// ArrayBuilder.hpp
// Clock Signal
//
// Created by Thomas Harte on 17/11/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#ifndef ArrayBuilder_hpp
#define ArrayBuilder_hpp
#include <functional>
#include <memory>
#include <vector>
#include "OpenGL.hpp"
namespace Outputs {
namespace CRT {
/*!
Owns two array buffers, an 'input' and an 'output' and vends pointers to allow an owner to write provisional data into those
plus a flush function to lock provisional data into place. Also supplies a submit method to transfer all currently locked
data to the GPU and bind_input/output methods to bind the internal buffers.
It is safe for one thread to communicate via the get_*_storage and flush inputs asynchronously from another that is making
use of the bind and submit outputs.
*/
class ArrayBuilder {
public:
/// Creates an instance of ArrayBuilder with @c output_size bytes of storage for the output buffer and
/// @c input_size bytes of storage for the input buffer.
ArrayBuilder(std::size_t input_size, std::size_t output_size);
/// Creates an instance of ArrayBuilder with @c output_size bytes of storage for the output buffer and
/// @c input_size bytes of storage for the input buffer that, rather than using OpenGL, will submit data
/// to the @c submission_function. [Teleological: this is provided as a testing hook.]
ArrayBuilder(std::size_t input_size, std::size_t output_size, std::function<void(bool is_input, uint8_t *, std::size_t)> submission_function);
/// Attempts to add @c size bytes to the input set.
/// @returns a pointer to the allocated area if allocation was possible; @c nullptr otherwise.
uint8_t *get_input_storage(std::size_t size);
/// Attempts to add @c size bytes to the output set.
/// @returns a pointer to the allocated area if allocation was possible; @c nullptr otherwise.
uint8_t *get_output_storage(std::size_t size);
/// @returns @c true if either of the input or output storage areas is currently exhausted; @c false otherwise.
bool is_full();
/// If neither input nor output was exhausted since the last flush, atomically commits both input and output
/// up to the currently allocated size for use upon the next @c submit, giving the supplied function a
/// chance to perform last-minute processing. Otherwise acts as a no-op.
void flush(const std::function<void(uint8_t *input, std::size_t input_size, uint8_t *output, std::size_t output_size)> &);
/// Binds the input array to GL_ARRAY_BUFFER.
void bind_input();
/// Binds the output array to GL_ARRAY_BUFFER.
void bind_output();
struct Submission {
std::size_t input_size, output_size;
};
/// Submits all flushed input and output data to the corresponding arrays.
/// @returns A @c Submission record, indicating how much data of each type was submitted.
Submission submit();
private:
class Buffer {
public:
Buffer(std::size_t size, std::function<void(bool is_input, uint8_t *, std::size_t)> submission_function);
~Buffer();
uint8_t *get_storage(std::size_t size);
uint8_t *get_unflushed(std::size_t &size);
void flush();
std::size_t submit(bool is_input);
void bind();
void reset();
private:
bool is_full = false;
GLuint buffer = 0;
std::function<void(bool is_input, uint8_t *, std::size_t)> submission_function_;
std::vector<uint8_t> data;
std::size_t allocated_data = 0;
std::size_t flushed_data = 0;
std::size_t submitted_data = 0;
} output_, input_;
uint8_t *get_storage(std::size_t size, Buffer &buffer);
bool is_full_ = false;
};
}
}
#endif /* ArrayBuilder_hpp */

View File

@ -1,52 +0,0 @@
//
// CRTContants.hpp
// Clock Signal
//
// Created by Thomas Harte on 19/03/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#ifndef CRTConstants_h
#define CRTConstants_h
#include "OpenGL.hpp"
#include <cstddef>
namespace Outputs {
namespace CRT {
// Output vertices are those used to copy from an input buffer, whether it describes data that maps directly to RGB
// or is one of the intermediate buffers that we've used to convert from composite towards RGB.
const GLsizei OutputVertexOffsetOfHorizontal = 0;
const GLsizei OutputVertexOffsetOfVertical = 4;
const GLsizei OutputVertexSize = 8;
// Input vertices, used only in composite mode, map from the input buffer to temporary buffer locations; such
// remapping occurs to ensure a continous stream of data for each scan, giving correct out-of-bounds behaviour
const GLsizei SourceVertexOffsetOfInputStart = 0;
const GLsizei SourceVertexOffsetOfOutputStart = 4;
const GLsizei SourceVertexOffsetOfEnds = 8;
const GLsizei SourceVertexOffsetOfPhaseTimeAndAmplitude = 12;
const GLsizei SourceVertexSize = 16;
// These constants hold the size of the rolling buffer to which the CPU writes
const GLsizei InputBufferBuilderWidth = 2048;
const GLsizei InputBufferBuilderHeight = 512;
// This is the size of the intermediate buffers used during composite to RGB conversion
const GLsizei IntermediateBufferWidth = 2048;
const GLsizei IntermediateBufferHeight = 512;
// Some internal buffer sizes
const GLsizeiptr OutputVertexBufferDataSize = OutputVertexSize * IntermediateBufferHeight; // i.e. the maximum number of scans of output that can be created between draws
const GLsizeiptr SourceVertexBufferDataSize = SourceVertexSize * IntermediateBufferHeight * 10; // (the maximum number of scans) * conservative, high guess at a maximumum number of events likely to occur within a scan
// TODO: when SourceVertexBufferDataSize is exhausted, the CRT keeps filling OutputVertexBufferDataSize regardless,
// leading to empty scanlines that nevertheless clear old contents.
}
}
#endif /* CRTContants_h */

View File

@ -1,527 +0,0 @@
// CRTOpenGL.cpp
// Clock Signal
//
// Created by Thomas Harte on 03/02/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#include "../CRT.hpp"
#include <cassert>
#include <cmath>
#include <cstdlib>
#include "CRTOpenGL.hpp"
#include "../../../SignalProcessing/FIRFilter.hpp"
#include "Shaders/OutputShader.hpp"
using namespace Outputs::CRT;
namespace {
static const GLenum source_data_texture_unit = GL_TEXTURE0;
static const GLenum pixel_accumulation_texture_unit = GL_TEXTURE1;
static const GLenum composite_texture_unit = GL_TEXTURE2;
static const GLenum separated_texture_unit = GL_TEXTURE3;
static const GLenum filtered_texture_unit = GL_TEXTURE4;
static const GLenum work_texture_unit = GL_TEXTURE2;
}
OpenGLOutputBuilder::OpenGLOutputBuilder(std::size_t bytes_per_pixel) :
visible_area_(Rect(0, 0, 1, 1)),
composite_src_output_y_(0),
last_output_width_(0),
last_output_height_(0),
fence_(nullptr),
texture_builder(bytes_per_pixel, source_data_texture_unit),
array_builder(SourceVertexBufferDataSize, OutputVertexBufferDataSize) {
glBlendFunc(GL_SRC_ALPHA, GL_CONSTANT_COLOR);
glBlendColor(0.4f, 0.4f, 0.4f, 1.0f);
// create the output vertex array
glGenVertexArrays(1, &output_vertex_array_);
// create the source vertex array
glGenVertexArrays(1, &source_vertex_array_);
// bool supports_texture_barrier = false;
#ifdef GL_NV_texture_barrier
// GLint number_of_extensions;
// glGetIntegerv(GL_NUM_EXTENSIONS, &number_of_extensions);
//
// for(GLuint c = 0; c < (GLuint)number_of_extensions; c++) {
// const char *extension_name = (const char *)glGetStringi(GL_EXTENSIONS, c);
// if(!std::strcmp(extension_name, "GL_NV_texture_barrier")) {
// supports_texture_barrier = true;
// }
// }
#endif
// if(supports_texture_barrier) {
// work_texture_.reset(new OpenGL::TextureTarget(IntermediateBufferWidth, IntermediateBufferHeight*2, work_texture_unit));
// } else {
composite_texture_.reset(new OpenGL::TextureTarget(IntermediateBufferWidth, IntermediateBufferHeight, composite_texture_unit, GL_NEAREST));
separated_texture_.reset(new OpenGL::TextureTarget(IntermediateBufferWidth, IntermediateBufferHeight, separated_texture_unit, GL_NEAREST));
filtered_texture_.reset(new OpenGL::TextureTarget(IntermediateBufferWidth, IntermediateBufferHeight, filtered_texture_unit, GL_LINEAR));
// }
}
OpenGLOutputBuilder::~OpenGLOutputBuilder() {
glDeleteVertexArrays(1, &output_vertex_array_);
}
void OpenGLOutputBuilder::set_target_framebuffer(GLint target_framebuffer) {
target_framebuffer_ = target_framebuffer;
}
void OpenGLOutputBuilder::draw_frame(unsigned int output_width, unsigned int output_height, bool only_if_dirty) {
// lock down any other draw_frames
draw_mutex_.lock();
// establish essentials
if(!output_shader_program_) {
prepare_composite_input_shaders();
prepare_svideo_input_shaders();
prepare_rgb_input_shaders();
prepare_source_vertex_array();
prepare_output_shader();
prepare_output_vertex_array();
set_timing_uniforms();
set_colour_space_uniforms();
set_gamma();
}
if(fence_ != nullptr) {
// if the GPU is still busy, don't wait; we'll catch it next time
if(glClientWaitSync(fence_, GL_SYNC_FLUSH_COMMANDS_BIT, only_if_dirty ? 0 : GL_TIMEOUT_IGNORED) == GL_TIMEOUT_EXPIRED) {
draw_mutex_.unlock();
return;
}
glDeleteSync(fence_);
}
// make sure everything is bound
composite_texture_->bind_texture();
separated_texture_->bind_texture();
filtered_texture_->bind_texture();
if(work_texture_) work_texture_->bind_texture();
// make sure there's a target to draw to
if(!framebuffer_ || static_cast<unsigned int>(framebuffer_->get_height()) != output_height || static_cast<unsigned int>(framebuffer_->get_width()) != output_width) {
std::unique_ptr<OpenGL::TextureTarget> new_framebuffer(new OpenGL::TextureTarget((GLsizei)output_width, (GLsizei)output_height, pixel_accumulation_texture_unit, GL_LINEAR));
if(framebuffer_) {
new_framebuffer->bind_framebuffer();
glClear(GL_COLOR_BUFFER_BIT);
glActiveTexture(pixel_accumulation_texture_unit);
framebuffer_->bind_texture();
framebuffer_->draw(static_cast<float>(output_width) / static_cast<float>(output_height));
new_framebuffer->bind_texture();
}
framebuffer_ = std::move(new_framebuffer);
}
// lock out the machine emulation until data is copied
output_mutex_.lock();
// release the mapping, giving up on trying to draw if data has been lost
ArrayBuilder::Submission array_submission = array_builder.submit();
// upload new source pixels, if any
glActiveTexture(source_data_texture_unit);
texture_builder.bind();
texture_builder.submit();
// buffer usage restart from 0 for the next time around
composite_src_output_y_ = 0;
// data having been grabbed, allow the machine to continue
output_mutex_.unlock();
struct RenderStage {
OpenGL::Shader *const shader;
OpenGL::TextureTarget *const target;
float clear_colour[3];
};
// for composite video, go through four steps to get to something that can be painted to the output
const RenderStage composite_render_stages[] = {
{composite_input_shader_program_.get(), composite_texture_.get(), {0.0, 0.0, 0.0}},
{composite_separation_filter_program_.get(), separated_texture_.get(), {0.0, 0.5, 0.5}},
{composite_chrominance_filter_shader_program_.get(), filtered_texture_.get(), {0.0, 0.0, 0.0}},
{nullptr, nullptr}
};
// for s-video, there are two steps: it's like composite but skips separation
const RenderStage svideo_render_stages[] = {
{svideo_input_shader_program_.get(), separated_texture_.get(), {0.0, 0.5, 0.5}},
{composite_chrominance_filter_shader_program_.get(), filtered_texture_.get(), {0.0, 0.0, 0.0}},
{nullptr, nullptr}
};
// for RGB video, there's also only two steps; a lowpass filter is still applied per physical reality
const RenderStage rgb_render_stages[] = {
{rgb_input_shader_program_.get(), composite_texture_.get(), {0.0, 0.0, 0.0}},
{rgb_filter_shader_program_.get(), filtered_texture_.get(), {0.0, 0.0, 0.0}},
{nullptr, nullptr}
};
const RenderStage *active_pipeline;
switch(video_signal_) {
default:
case VideoSignal::Composite: active_pipeline = composite_render_stages; break;
case VideoSignal::SVideo: active_pipeline = svideo_render_stages; break;
case VideoSignal::RGB: active_pipeline = rgb_render_stages; break;
}
if(array_submission.input_size || array_submission.output_size) {
// all drawing will be from the source vertex array and without blending
glBindVertexArray(source_vertex_array_);
glDisable(GL_BLEND);
#ifdef GL_NV_texture_barrier
// if(work_texture_) {
// work_texture_->bind_framebuffer();
// glClear(GL_COLOR_BUFFER_BIT);
// }
#endif
while(active_pipeline->shader) {
// switch to the framebuffer and shader associated with this stage
active_pipeline->shader->bind();
if(!work_texture_) {
active_pipeline->target->bind_framebuffer();
// if this is the final stage before painting to the CRT, clear the framebuffer before drawing in order to blank out
// those portions for which no input was provided
// if(!active_pipeline[1].shader) {
glClearColor(active_pipeline->clear_colour[0], active_pipeline->clear_colour[1], active_pipeline->clear_colour[2], 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// }
}
// draw
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, (GLsizei)array_submission.input_size / SourceVertexSize);
active_pipeline++;
#ifdef GL_NV_texture_barrier
// glTextureBarrierNV();
#endif
}
// prepare to transfer to framebuffer
framebuffer_->bind_framebuffer();
// draw from the output array buffer, with blending
glBindVertexArray(output_vertex_array_);
glEnable(GL_BLEND);
// update uniforms, then bind the target
if(last_output_width_ != output_width || last_output_height_ != output_height) {
output_shader_program_->set_output_size(output_width, output_height, visible_area_);
last_output_width_ = output_width;
last_output_height_ = output_height;
// Configure right and left gutters to crop the left- and right-hand 1% of the display.
left_overlay_.reset(new OpenGL::Rectangle(output_shader_program_->get_left_extent() * 0.98f, -1.0f, -1.0f, 2.0f));
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);
left_overlay_->draw(0.0, 0.0, 0.0);
right_overlay_->draw(0.0, 0.0, 0.0);
}
#ifdef GL_NV_texture_barrier
// glTextureBarrierNV();
#endif
// Copy framebuffer to the intended place; apply a threshold so that any persistent errors in
// the lower part of the colour channels are invisible.
glDisable(GL_BLEND);
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(target_framebuffer_));
glViewport(0, 0, (GLsizei)output_width, (GLsizei)output_height);
glActiveTexture(pixel_accumulation_texture_unit);
framebuffer_->bind_texture();
framebuffer_->draw(static_cast<float>(output_width) / static_cast<float>(output_height), 4.0f / 255.0f);
fence_ = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
draw_mutex_.unlock();
}
void OpenGLOutputBuilder::reset_all_OpenGL_state() {
composite_input_shader_program_ = nullptr;
composite_separation_filter_program_ = nullptr;
composite_chrominance_filter_shader_program_ = nullptr;
svideo_input_shader_program_ = nullptr;
rgb_input_shader_program_ = nullptr;
rgb_filter_shader_program_ = nullptr;
output_shader_program_ = nullptr;
framebuffer_ = nullptr;
last_output_width_ = last_output_height_ = 0;
}
void OpenGLOutputBuilder::set_openGL_context_will_change(bool should_delete_resources) {
output_mutex_.lock();
reset_all_OpenGL_state();
output_mutex_.unlock();
}
void OpenGLOutputBuilder::set_composite_sampling_function(const std::string &shader) {
std::lock_guard<std::mutex> lock_guard(output_mutex_);
composite_shader_ = shader;
reset_all_OpenGL_state();
}
void OpenGLOutputBuilder::set_svideo_sampling_function(const std::string &shader) {
std::lock_guard<std::mutex> lock_guard(output_mutex_);
svideo_shader_ = shader;
reset_all_OpenGL_state();
}
void OpenGLOutputBuilder::set_rgb_sampling_function(const std::string &shader) {
std::lock_guard<std::mutex> lock_guard(output_mutex_);
rgb_shader_ = shader;
reset_all_OpenGL_state();
}
// MARK: - Program compilation
void OpenGLOutputBuilder::prepare_composite_input_shaders() {
composite_input_shader_program_ = OpenGL::IntermediateShader::make_composite_source_shader(composite_shader_, svideo_shader_, rgb_shader_);
composite_input_shader_program_->set_source_texture_unit(source_data_texture_unit);
composite_input_shader_program_->set_output_size(IntermediateBufferWidth, IntermediateBufferHeight);
composite_separation_filter_program_ = OpenGL::IntermediateShader::make_chroma_luma_separation_shader();
composite_separation_filter_program_->set_source_texture_unit(work_texture_ ? work_texture_unit : composite_texture_unit);
composite_separation_filter_program_->set_output_size(IntermediateBufferWidth, IntermediateBufferHeight);
composite_chrominance_filter_shader_program_ = OpenGL::IntermediateShader::make_chroma_filter_shader();
composite_chrominance_filter_shader_program_->set_source_texture_unit(work_texture_ ? work_texture_unit : separated_texture_unit);
composite_chrominance_filter_shader_program_->set_output_size(IntermediateBufferWidth, IntermediateBufferHeight);
// TODO: the below is related to texture fencing, which is not yet implemented correctly, so not yet enabled.
if(work_texture_) {
composite_input_shader_program_->set_is_double_height(true, 0.0f, 0.0f);
composite_separation_filter_program_->set_is_double_height(true, 0.0f, 0.5f);
composite_chrominance_filter_shader_program_->set_is_double_height(true, 0.5f, 0.0f);
} else {
composite_input_shader_program_->set_is_double_height(false);
composite_separation_filter_program_->set_is_double_height(false);
composite_chrominance_filter_shader_program_->set_is_double_height(false);
}
}
void OpenGLOutputBuilder::prepare_svideo_input_shaders() {
if(!svideo_shader_.empty() || !rgb_shader_.empty()) {
svideo_input_shader_program_ = OpenGL::IntermediateShader::make_svideo_source_shader(svideo_shader_, rgb_shader_);
svideo_input_shader_program_->set_source_texture_unit(source_data_texture_unit);
svideo_input_shader_program_->set_output_size(IntermediateBufferWidth, IntermediateBufferHeight);
// TODO: the below is related to texture fencing, which is not yet implemented correctly, so not yet enabled.
if(work_texture_) {
svideo_input_shader_program_->set_is_double_height(true, 0.0f, 0.0f);
} else {
svideo_input_shader_program_->set_is_double_height(false);
}
}
}
void OpenGLOutputBuilder::prepare_rgb_input_shaders() {
if(!rgb_shader_.empty()) {
rgb_input_shader_program_ = OpenGL::IntermediateShader::make_rgb_source_shader(rgb_shader_);
rgb_input_shader_program_->set_source_texture_unit(source_data_texture_unit);
rgb_input_shader_program_->set_output_size(IntermediateBufferWidth, IntermediateBufferHeight);
rgb_filter_shader_program_ = OpenGL::IntermediateShader::make_rgb_filter_shader();
rgb_filter_shader_program_->set_source_texture_unit(composite_texture_unit);
rgb_filter_shader_program_->set_output_size(IntermediateBufferWidth, IntermediateBufferHeight);
}
}
void OpenGLOutputBuilder::prepare_source_vertex_array() {
if(composite_input_shader_program_ || svideo_input_shader_program_) {
glBindVertexArray(source_vertex_array_);
array_builder.bind_input();
}
using Shader = OpenGL::IntermediateShader;
OpenGL::IntermediateShader *const shaders[] = {
composite_input_shader_program_.get(),
svideo_input_shader_program_.get()
};
for(int c = 0; c < 2; ++c) {
if(!shaders[c]) continue;
shaders[c]->enable_vertex_attribute_with_pointer(
Shader::get_input_name(Shader::Input::InputStart),
2, GL_UNSIGNED_SHORT, GL_FALSE, SourceVertexSize,
(void *)SourceVertexOffsetOfInputStart, 1);
shaders[c]->enable_vertex_attribute_with_pointer(
Shader::get_input_name(Shader::Input::OutputStart),
2, GL_UNSIGNED_SHORT, GL_FALSE, SourceVertexSize,
(void *)SourceVertexOffsetOfOutputStart, 1);
shaders[c]->enable_vertex_attribute_with_pointer(
Shader::get_input_name(Shader::Input::Ends),
2, GL_UNSIGNED_SHORT, GL_FALSE, SourceVertexSize,
(void *)SourceVertexOffsetOfEnds, 1);
shaders[c]->enable_vertex_attribute_with_pointer(
Shader::get_input_name(Shader::Input::PhaseTimeAndAmplitude),
3, GL_UNSIGNED_BYTE, GL_FALSE, SourceVertexSize,
(void *)SourceVertexOffsetOfPhaseTimeAndAmplitude, 1);
}
}
void OpenGLOutputBuilder::prepare_output_shader() {
output_shader_program_ = OpenGL::OutputShader::make_shader("", "texture(texID, srcCoordinatesVarying).rgb", false);
output_shader_program_->set_source_texture_unit(work_texture_ ? work_texture_unit : filtered_texture_unit);
// output_shader_program_->set_source_texture_unit(composite_texture_unit);
output_shader_program_->set_origin_is_double_height(!!work_texture_);
}
void OpenGLOutputBuilder::prepare_output_vertex_array() {
if(output_shader_program_) {
glBindVertexArray(output_vertex_array_);
array_builder.bind_output();
using Shader = OpenGL::OutputShader;
output_shader_program_->enable_vertex_attribute_with_pointer(
Shader::get_input_name(Shader::Input::Horizontal),
2, GL_UNSIGNED_SHORT, GL_FALSE, OutputVertexSize,
(void *)OutputVertexOffsetOfHorizontal, 1);
output_shader_program_->enable_vertex_attribute_with_pointer(
Shader::get_input_name(Shader::Input::Vertical),
2, GL_UNSIGNED_SHORT, GL_FALSE, OutputVertexSize,
(void *)OutputVertexOffsetOfVertical, 1);
}
}
// MARK: - Public Configuration
void OpenGLOutputBuilder::set_video_signal(VideoSignal video_signal) {
if(video_signal_ != video_signal) {
video_signal_ = video_signal;
composite_src_output_y_ = 0;
last_output_width_ = 0;
last_output_height_ = 0;
set_output_shader_width();
}
}
void OpenGLOutputBuilder::set_timing(unsigned int input_frequency, unsigned int cycles_per_line, unsigned int height_of_display, unsigned int horizontal_scan_period, unsigned int vertical_scan_period, unsigned int vertical_period_divider) {
std::lock_guard<std::mutex> lock_guard(output_mutex_);
input_frequency_ = input_frequency;
cycles_per_line_ = cycles_per_line;
height_of_display_ = height_of_display;
horizontal_scan_period_ = horizontal_scan_period;
vertical_scan_period_ = vertical_scan_period;
vertical_period_divider_ = vertical_period_divider;
set_timing_uniforms();
}
// MARK: - Internal Configuration
void OpenGLOutputBuilder::set_colour_space_uniforms() {
GLfloat rgbToYUV[] = {0.299f, -0.14713f, 0.615f, 0.587f, -0.28886f, -0.51499f, 0.114f, 0.436f, -0.10001f};
GLfloat yuvToRGB[] = {1.0f, 1.0f, 1.0f, 0.0f, -0.39465f, 2.03211f, 1.13983f, -0.58060f, 0.0f};
GLfloat rgbToYIQ[] = {0.299f, 0.596f, 0.211f, 0.587f, -0.274f, -0.523f, 0.114f, -0.322f, 0.312f};
GLfloat yiqToRGB[] = {1.0f, 1.0f, 1.0f, 0.956f, -0.272f, -1.106f, 0.621f, -0.647f, 1.703f};
GLfloat *fromRGB = nullptr, *toRGB = nullptr;
switch(colour_space_) {
case ColourSpace::YIQ:
fromRGB = rgbToYIQ;
toRGB = yiqToRGB;
break;
case ColourSpace::YUV:
fromRGB = rgbToYUV;
toRGB = yuvToRGB;
break;
default: assert(false); break;
}
if(composite_input_shader_program_) composite_input_shader_program_->set_colour_conversion_matrices(fromRGB, toRGB);
if(composite_separation_filter_program_) composite_separation_filter_program_->set_colour_conversion_matrices(fromRGB, toRGB);
if(composite_chrominance_filter_shader_program_) composite_chrominance_filter_shader_program_->set_colour_conversion_matrices(fromRGB, toRGB);
if(svideo_input_shader_program_) svideo_input_shader_program_->set_colour_conversion_matrices(fromRGB, toRGB);
}
void OpenGLOutputBuilder::set_gamma() {
if(output_shader_program_) output_shader_program_->set_gamma_ratio(gamma_);
}
/*!
@returns The multiplier to apply to x positions received at the shader in order to produce locations in the intermediate
texture. Intermediate textures are in phase with the composite signal, so this is a function of (i) composite frequency
(determining how much of the texture adds up to a single line); and (ii) input frequency (determining what the input
positions mean as a fraction of a line).
*/
float OpenGLOutputBuilder::get_composite_output_width() const {
return
(static_cast<float>(colour_cycle_numerator_ * 4) / static_cast<float>(colour_cycle_denominator_ * IntermediateBufferWidth)) *
(static_cast<float>(IntermediateBufferWidth) / static_cast<float>(cycles_per_line_));
}
void OpenGLOutputBuilder::set_output_shader_width() {
if(output_shader_program_) {
// For anything that isn't RGB, scale so that sampling is in-phase with the colour subcarrier.
const float width = (video_signal_ == VideoSignal::RGB) ? 1.0f : get_composite_output_width();
output_shader_program_->set_input_width_scaler(width);
}
}
void OpenGLOutputBuilder::set_timing_uniforms() {
const float colour_subcarrier_frequency = static_cast<float>(colour_cycle_numerator_) / static_cast<float>(colour_cycle_denominator_);
const float output_width = get_composite_output_width();
const float sample_cycles_per_line = cycles_per_line_ / output_width;
if(composite_separation_filter_program_) {
composite_separation_filter_program_->set_width_scalers(output_width, output_width);
composite_separation_filter_program_->set_separation_frequency(sample_cycles_per_line, colour_subcarrier_frequency);
composite_separation_filter_program_->set_extension(6.0f);
}
if(composite_chrominance_filter_shader_program_) {
composite_chrominance_filter_shader_program_->set_width_scalers(output_width, output_width);
composite_chrominance_filter_shader_program_->set_extension(5.0f);
}
if(rgb_filter_shader_program_) {
rgb_filter_shader_program_->set_width_scalers(1.0f, 1.0f);
rgb_filter_shader_program_->set_filter_coefficients(sample_cycles_per_line, static_cast<float>(input_frequency_) * 0.5f);
}
if(output_shader_program_) {
set_output_shader_width();
output_shader_program_->set_timing(height_of_display_, cycles_per_line_, horizontal_scan_period_, vertical_scan_period_, vertical_period_divider_);
}
if(composite_input_shader_program_) {
composite_input_shader_program_->set_width_scalers(1.0f, output_width);
composite_input_shader_program_->set_extension(0.0f);
}
if(svideo_input_shader_program_) {
svideo_input_shader_program_->set_width_scalers(1.0f, output_width);
svideo_input_shader_program_->set_extension(0.0f);
}
if(rgb_input_shader_program_) {
rgb_input_shader_program_->set_width_scalers(1.0f, 1.0f);
}
}

View File

@ -1,180 +0,0 @@
//
// CRTOpenGL.hpp
// Clock Signal
//
// Created by Thomas Harte on 13/02/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#ifndef CRTOpenGL_h
#define CRTOpenGL_h
#include "../CRTTypes.hpp"
#include "CRTConstants.hpp"
#include "OpenGL.hpp"
#include "TextureTarget.hpp"
#include "Shaders/Shader.hpp"
#include "ArrayBuilder.hpp"
#include "TextureBuilder.hpp"
#include "Shaders/OutputShader.hpp"
#include "Shaders/IntermediateShader.hpp"
#include "Rectangle.hpp"
#include <mutex>
#include <vector>
namespace Outputs {
namespace CRT {
class OpenGLOutputBuilder {
private:
// colour information
ColourSpace colour_space_;
unsigned int colour_cycle_numerator_;
unsigned int colour_cycle_denominator_;
VideoSignal video_signal_;
float gamma_;
// timing information to allow reasoning about input information
unsigned int input_frequency_;
unsigned int cycles_per_line_;
unsigned int height_of_display_;
unsigned int horizontal_scan_period_;
unsigned int vertical_scan_period_;
unsigned int vertical_period_divider_;
// The user-supplied visible area
Rect visible_area_;
// Other things the caller may have provided.
std::string composite_shader_;
std::string svideo_shader_;
std::string rgb_shader_;
GLint target_framebuffer_ = 0;
// Methods used by the OpenGL code
void prepare_output_shader();
void prepare_rgb_input_shaders();
void prepare_svideo_input_shaders();
void prepare_composite_input_shaders();
void prepare_output_vertex_array();
void prepare_source_vertex_array();
// the run and input data buffers
std::mutex output_mutex_;
std::mutex draw_mutex_;
// transient buffers indicating composite data not yet decoded
GLsizei composite_src_output_y_;
std::unique_ptr<OpenGL::OutputShader> output_shader_program_;
std::unique_ptr<OpenGL::IntermediateShader> composite_input_shader_program_;
std::unique_ptr<OpenGL::IntermediateShader> composite_separation_filter_program_;
std::unique_ptr<OpenGL::IntermediateShader> composite_chrominance_filter_shader_program_;
std::unique_ptr<OpenGL::IntermediateShader> svideo_input_shader_program_;
std::unique_ptr<OpenGL::IntermediateShader> rgb_input_shader_program_;
std::unique_ptr<OpenGL::IntermediateShader> rgb_filter_shader_program_;
std::unique_ptr<OpenGL::TextureTarget> composite_texture_; // receives raw composite levels
std::unique_ptr<OpenGL::TextureTarget> separated_texture_; // receives filtered Y in the R channel plus unfiltered but demodulated chrominance in G and B
std::unique_ptr<OpenGL::TextureTarget> filtered_texture_; // receives filtered YIQ or YUV
std::unique_ptr<OpenGL::TextureTarget> work_texture_; // used for all intermediate rendering if texture fences are supported
std::unique_ptr<OpenGL::TextureTarget> framebuffer_; // the current pixel output
GLuint output_vertex_array_;
GLuint source_vertex_array_;
unsigned int last_output_width_, last_output_height_;
void set_timing_uniforms();
void set_colour_space_uniforms();
void set_gamma();
void establish_OpenGL_state();
void reset_all_OpenGL_state();
GLsync fence_;
float get_composite_output_width() const;
void set_output_shader_width();
// 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_;
std::unique_ptr<OpenGL::Rectangle> left_overlay_;
public:
// These two are protected by output_mutex_.
TextureBuilder texture_builder;
ArrayBuilder array_builder;
OpenGLOutputBuilder(std::size_t bytes_per_pixel);
~OpenGLOutputBuilder();
inline void set_colour_format(ColourSpace colour_space, unsigned int colour_cycle_numerator, unsigned int colour_cycle_denominator) {
std::lock_guard<std::mutex> output_guard(output_mutex_);
colour_space_ = colour_space;
colour_cycle_numerator_ = colour_cycle_numerator;
colour_cycle_denominator_ = colour_cycle_denominator;
set_colour_space_uniforms();
}
inline void set_visible_area(Rect visible_area) {
visible_area_ = visible_area;
}
inline void set_gamma(float gamma) {
gamma_ = gamma;
set_gamma();
}
inline std::unique_lock<std::mutex> get_output_lock() {
return std::unique_lock<std::mutex>(output_mutex_);
}
inline VideoSignal get_output_device() {
return video_signal_;
}
inline uint16_t get_composite_output_y() {
return static_cast<uint16_t>(composite_src_output_y_);
}
inline bool composite_output_buffer_is_full() {
return composite_src_output_y_ == IntermediateBufferHeight;
}
inline void increment_composite_output_y() {
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);
void set_composite_sampling_function(const std::string &);
void set_svideo_sampling_function(const std::string &);
void set_rgb_sampling_function(const std::string &);
void set_video_signal(VideoSignal);
void set_timing(unsigned int input_frequency, unsigned int cycles_per_line, unsigned int height_of_display, unsigned int horizontal_scan_period, unsigned int vertical_scan_period, unsigned int vertical_period_divider);
};
}
}
#endif /* CRTOpenGL_h */

View File

@ -9,7 +9,9 @@
#ifndef Flywheel_hpp
#define Flywheel_hpp
#include <cassert>
#include <cstdlib>
#include <cstdint>
namespace Outputs {
namespace CRT {
@ -29,14 +31,12 @@ struct Flywheel {
@param retrace_time The amount of time it takes to complete a retrace.
@param sync_error_window The permitted deviation of sync timings from the norm.
*/
Flywheel(unsigned int standard_period, unsigned int retrace_time, unsigned int sync_error_window) :
Flywheel(int standard_period, int retrace_time, int sync_error_window) :
standard_period_(standard_period),
retrace_time_(retrace_time),
sync_error_window_(sync_error_window),
counter_(0),
counter_before_retrace_(standard_period - retrace_time),
expected_next_sync_(standard_period),
number_of_surprises_(0) {}
expected_next_sync_(standard_period) {}
enum SyncEvent {
/// Indicates that no synchronisation events will occur in the queried window.
@ -60,14 +60,14 @@ struct Flywheel {
@returns The next synchronisation event.
*/
inline SyncEvent get_next_event_in_period(bool sync_is_requested, unsigned int cycles_to_run_for, unsigned int *cycles_advanced) {
inline SyncEvent get_next_event_in_period(bool sync_is_requested, int cycles_to_run_for, int *cycles_advanced) {
// do we recognise this hsync, thereby adjusting future time expectations?
if(sync_is_requested) {
if(counter_ < sync_error_window_ || counter_ > expected_next_sync_ - sync_error_window_) {
unsigned int time_now = (counter_ < sync_error_window_) ? expected_next_sync_ + counter_ : counter_;
const int time_now = (counter_ < sync_error_window_) ? expected_next_sync_ + counter_ : counter_;
expected_next_sync_ = (3*expected_next_sync_ + time_now) >> 2;
} else {
number_of_surprises_++;
++number_of_surprises_;
if(counter_ < retrace_time_ + (expected_next_sync_ >> 1)) {
expected_next_sync_ = (3*expected_next_sync_ + standard_period_ + sync_error_window_) >> 2;
@ -78,15 +78,15 @@ struct Flywheel {
}
SyncEvent proposed_event = SyncEvent::None;
unsigned int proposed_sync_time = cycles_to_run_for;
int proposed_sync_time = cycles_to_run_for;
// will we end an ongoing retrace?
// End an ongoing retrace?
if(counter_ < retrace_time_ && counter_ + proposed_sync_time >= retrace_time_) {
proposed_sync_time = retrace_time_ - counter_;
proposed_event = SyncEvent::EndRetrace;
}
// will we start a retrace?
// Start a retrace?
if(counter_ + proposed_sync_time >= expected_next_sync_) {
proposed_sync_time = expected_next_sync_ - counter_;
proposed_event = SyncEvent::StartRetrace;
@ -104,8 +104,13 @@ struct Flywheel {
@param event The synchronisation event to apply after that period.
*/
inline void apply_event(unsigned int cycles_advanced, SyncEvent event) {
inline void apply_event(int cycles_advanced, SyncEvent event) {
// In debug builds, perform a sanity check for counter overflow.
#ifndef NDEBUG
const int old_counter = counter_;
#endif
counter_ += cycles_advanced;
assert(old_counter <= counter_);
switch(event) {
default: return;
@ -122,9 +127,9 @@ struct Flywheel {
@returns The current output position.
*/
inline unsigned int get_current_output_position() {
inline int get_current_output_position() {
if(counter_ < retrace_time_) {
unsigned int retrace_distance = (counter_ * standard_period_) / retrace_time_;
const int retrace_distance = int((int64_t(counter_) * int64_t(standard_period_)) / int64_t(retrace_time_));
if(retrace_distance > counter_before_retrace_) return 0;
return counter_before_retrace_ - retrace_distance;
}
@ -135,7 +140,7 @@ struct Flywheel {
/*!
@returns the amount of time since retrace last began. Time then counts monotonically up from zero.
*/
inline unsigned int get_current_time() {
inline int get_current_time() {
return counter_;
}
@ -149,14 +154,14 @@ struct Flywheel {
/*!
@returns the expected length of the scan period (excluding retrace).
*/
inline unsigned int get_scan_period() {
inline int get_scan_period() {
return standard_period_ - retrace_time_;
}
/*!
@returns the expected length of a complete scan and retrace cycle.
*/
inline unsigned int get_standard_period() {
inline int get_standard_period() {
return standard_period_;
}
@ -164,29 +169,31 @@ struct Flywheel {
@returns the number of synchronisation events that have seemed surprising since the last time this method was called;
a low number indicates good synchronisation.
*/
inline unsigned int get_and_reset_number_of_surprises() {
unsigned int result = number_of_surprises_;
inline int get_and_reset_number_of_surprises() {
const int result = number_of_surprises_;
number_of_surprises_ = 0;
return result;
}
/*!
@returns `true` if a sync is expected soon or the time at which it was expected was recent.
@returns `true` if a sync is expected soon or if the time at which it was expected (or received) was recent.
*/
inline bool is_near_expected_sync() {
return abs(static_cast<int>(counter_) - static_cast<int>(expected_next_sync_)) < static_cast<int>(standard_period_) / 50;
return
(counter_ < (standard_period_ / 100)) ||
(counter_ >= expected_next_sync_ - (standard_period_ / 100));
}
private:
unsigned int standard_period_; // the normal length of time between syncs
const unsigned int retrace_time_; // a constant indicating the amount of time it takes to perform a retrace
const unsigned int sync_error_window_; // a constant indicating the window either side of the next expected sync in which we'll accept other syncs
const int standard_period_; // the normal length of time between syncs
const int retrace_time_; // a constant indicating the amount of time it takes to perform a retrace
const int sync_error_window_; // a constant indicating the window either side of the next expected sync in which we'll accept other syncs
unsigned int counter_; // time since the _start_ of the last sync
unsigned int counter_before_retrace_; // the value of _counter immediately before retrace began
unsigned int expected_next_sync_; // our current expection of when the next sync will be encountered (which implies velocity)
int counter_ = 0; // time since the _start_ of the last sync
int counter_before_retrace_; // the value of _counter immediately before retrace began
int expected_next_sync_; // our current expection of when the next sync will be encountered (which implies velocity)
unsigned int number_of_surprises_; // a count of the surprising syncs
int number_of_surprises_ = 0; // a count of the surprising syncs
/*
Implementation notes:

View File

@ -1,25 +0,0 @@
//
// OpenGL.h
// Clock Signal
//
// Created by Thomas Harte on 07/02/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#ifndef OpenGL_h
#define OpenGL_h
// TODO: figure out correct include paths for other platforms.
#ifdef __APPLE__
#if TARGET_OS_IPHONE
#else
#include <OpenGL/OpenGL.h>
#include <OpenGL/gl3.h>
#include <OpenGL/gl3ext.h>
#endif
#else
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#endif
#endif /* OpenGL_h */

View File

@ -1,435 +0,0 @@
//
// IntermediateShader.cpp
// Clock Signal
//
// Created by Thomas Harte on 28/04/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#include "IntermediateShader.hpp"
#include <cassert>
#include <cstring>
#include <sstream>
#include "../../../../SignalProcessing/FIRFilter.hpp"
using namespace OpenGL;
std::string IntermediateShader::get_input_name(Input input) {
switch(input) {
case Input::InputStart: return "inputStart";
case Input::OutputStart: return "outputStart";
case Input::Ends: return "ends";
case Input::PhaseTimeAndAmplitude: return "phaseTimeAndAmplitude";
// Intended to be unreachable.
default: assert(false); return "";
}
}
std::unique_ptr<IntermediateShader> IntermediateShader::make_shader(const std::string &fragment_shader, bool use_usampler, bool input_is_inputPosition) {
std::ostringstream vertex_shader;
vertex_shader <<
"#version 150\n"
"in vec2 " << get_input_name(Input::InputStart) << ";"
"in vec2 " << get_input_name(Input::OutputStart) << ";"
"in vec2 " << get_input_name(Input::Ends) << ";"
"in vec3 " << get_input_name(Input::PhaseTimeAndAmplitude) << ";"
"uniform ivec2 outputTextureSize;"
"uniform float extension;"
"uniform " << (use_usampler ? "usampler2D" : "sampler2D") << " texID;"
"uniform float offsets[5];"
"uniform vec2 widthScalers;"
"uniform float inputVerticalOffset;"
"uniform float outputVerticalOffset;"
"uniform float textureHeightDivisor;"
"out vec3 phaseAndAmplitudeVarying;"
"out vec2 inputPositionsVarying[11];"
"out vec2 delayLinePositionVarying;"
"void main(void)"
"{"
// odd vertices are on the left, even on the right
"float extent = float(gl_VertexID & 1);"
"float longitudinal = float((gl_VertexID & 2) >> 1);"
// inputPosition.x is either inputStart.x or ends.x, depending on whether it is on the left or the right;
// outputPosition.x is either outputStart.x or ends.y;
// .ys are inputStart.y and outputStart.y respectively
"vec2 inputPosition = vec2(mix(inputStart.x, ends.x, extent)*widthScalers[0], inputStart.y + inputVerticalOffset);"
"vec2 outputPosition = vec2(mix(outputStart.x, ends.y, extent)*widthScalers[1], outputStart.y + outputVerticalOffset);"
"inputPosition.y += longitudinal;"
"outputPosition.y += longitudinal;"
// extension is the amount to extend both the input and output by to add a full colour cycle at each end
"vec2 extensionVector = vec2(extension, 0.0) * 2.0 * (extent - 0.5);"
// extended[Input/Output]Position are [input/output]Position with the necessary applied extension
"vec2 extendedInputPosition = " << (input_is_inputPosition ? "inputPosition" : "outputPosition") << " + extensionVector;"
"vec2 extendedOutputPosition = outputPosition + extensionVector;"
// scale mappedInputPosition to the ordinary normalised range
"vec2 textureSize = vec2(textureSize(texID, 0));"
"vec2 mappedInputPosition = extendedInputPosition / textureSize;"
// setup input positions spaced as per the supplied offsets; these are for filtering where required
"inputPositionsVarying[0] = mappedInputPosition - (vec2(5.0, 0.0) / textureSize);"
"inputPositionsVarying[1] = mappedInputPosition - (vec2(4.0, 0.0) / textureSize);"
"inputPositionsVarying[2] = mappedInputPosition - (vec2(3.0, 0.0) / textureSize);"
"inputPositionsVarying[3] = mappedInputPosition - (vec2(2.0, 0.0) / textureSize);"
"inputPositionsVarying[4] = mappedInputPosition - (vec2(1.0, 0.0) / textureSize);"
"inputPositionsVarying[5] = mappedInputPosition;"
"inputPositionsVarying[6] = mappedInputPosition + (vec2(1.0, 0.0) / textureSize);"
"inputPositionsVarying[7] = mappedInputPosition + (vec2(2.0, 0.0) / textureSize);"
"inputPositionsVarying[8] = mappedInputPosition + (vec2(3.0, 0.0) / textureSize);"
"inputPositionsVarying[9] = mappedInputPosition + (vec2(4.0, 0.0) / textureSize);"
"inputPositionsVarying[10] = mappedInputPosition + (vec2(5.0, 0.0) / textureSize);"
"delayLinePositionVarying = mappedInputPosition - vec2(0.0, 1.0);"
// setup phaseAndAmplitudeVarying.x as colour burst subcarrier phase, in radians;
// setup phaseAndAmplitudeVarying.y as colour burst amplitude;
// setup phaseAndAmplitudeVarying.z as 1 / abs(colour burst amplitude), or 0.0 if amplitude is 0.0;
"phaseAndAmplitudeVarying.x = (extendedOutputPosition.x + (phaseTimeAndAmplitude.x / 64.0)) * 0.5 * 3.141592654;"
"phaseAndAmplitudeVarying.y = (phaseTimeAndAmplitude.y - 128) / 127.0;"
"phaseAndAmplitudeVarying.z = (abs(phaseAndAmplitudeVarying.y) > 0.05) ? 1.0 / abs(phaseAndAmplitudeVarying.y) : 0.0;"
// determine output position by scaling the output position according to the texture size
"vec2 eyePosition = 2.0*(extendedOutputPosition / outputTextureSize) - vec2(1.0);"
"gl_Position = vec4(eyePosition, 0.0, 1.0);"
"}";
return std::unique_ptr<IntermediateShader>(new IntermediateShader(vertex_shader.str(), fragment_shader, {
{get_input_name(Input::InputStart), 0},
{get_input_name(Input::OutputStart), 1},
{get_input_name(Input::Ends), 2},
{get_input_name(Input::PhaseTimeAndAmplitude), 3}
}));
}
std::unique_ptr<IntermediateShader> IntermediateShader::make_composite_source_shader(const std::string &composite_shader, const std::string &svideo_shader, const std::string &rgb_shader) {
std::ostringstream fragment_shader;
fragment_shader <<
"#version 150\n"
"in vec2 inputPositionsVarying[11];"
"in vec3 phaseAndAmplitudeVarying;"
"out vec4 fragColour;"
"uniform usampler2D texID;"
<< composite_shader;
if(composite_shader.empty()) {
if(!svideo_shader.empty()) {
fragment_shader <<
svideo_shader <<
"float composite_sample(usampler2D texID, vec2 coordinate, float phase, float amplitude)"
"{"
"vec2 svideoColour = svideo_sample(texID, coordinate, phase, amplitude);"
"return mix(svideoColour.x, svideoColour.y, abs(amplitude));"
"}";
} else {
fragment_shader <<
rgb_shader <<
"uniform mat3 rgbToLumaChroma;"
"float composite_sample(usampler2D texID, vec2 coordinate, float phase, float amplitude)"
"{"
"vec3 rgbColour = clamp(rgb_sample(texID, coordinate), vec3(0.0), vec3(1.0));"
"vec3 lumaChromaColour = rgbToLumaChroma * rgbColour;"
"vec2 quadrature = vec2(cos(phase), sin(phase)) * vec2(abs(amplitude), amplitude);"
"return dot(lumaChromaColour, vec3(1.0 - abs(amplitude), quadrature));"
"}";
}
}
fragment_shader <<
"void main(void)"
"{"
"fragColour = vec4(composite_sample(texID, inputPositionsVarying[5], phaseAndAmplitudeVarying.x, phaseAndAmplitudeVarying.y));"
"}";
return make_shader(fragment_shader.str(), true, true);
}
std::unique_ptr<IntermediateShader> IntermediateShader::make_svideo_source_shader(const std::string &svideo_shader, const std::string &rgb_shader) {
std::ostringstream fragment_shader;
fragment_shader <<
"#version 150\n"
"in vec2 inputPositionsVarying[11];"
"in vec3 phaseAndAmplitudeVarying;"
"out vec3 fragColour;"
"uniform usampler2D texID;"
<< svideo_shader;
if(svideo_shader.empty()) {
fragment_shader
<< rgb_shader <<
"uniform mat3 rgbToLumaChroma;"
"vec2 svideo_sample(usampler2D texID, vec2 coordinate, float phase, float amplitude)"
"{"
"vec3 rgbColour = clamp(rgb_sample(texID, coordinate), vec3(0.0), vec3(1.0));"
"vec3 lumaChromaColour = rgbToLumaChroma * rgbColour;"
"vec2 quadrature = vec2(cos(phase), sin(phase)) * vec2(1.0, sign(amplitude));"
"return vec2(lumaChromaColour.x, 0.5 + dot(quadrature, lumaChromaColour.yz) * 0.5);"
"}";
}
fragment_shader <<
"void main(void)"
"{"
"vec2 sample = svideo_sample(texID, inputPositionsVarying[5], phaseAndAmplitudeVarying.x, phaseAndAmplitudeVarying.y);"
"vec2 quadrature = vec2(cos(phaseAndAmplitudeVarying.x), sin(phaseAndAmplitudeVarying.x)) * vec2(1.0, sign(phaseAndAmplitudeVarying.y)) * 0.5 * phaseAndAmplitudeVarying.z;"
"fragColour = vec3(sample.x, vec2(0.5) + (sample.y * quadrature));"
"}";
return make_shader(fragment_shader.str(), true, true);
}
std::unique_ptr<IntermediateShader> IntermediateShader::make_rgb_source_shader(const std::string &rgb_shader) {
std::ostringstream fragment_shader;
fragment_shader <<
"#version 150\n"
"in vec2 inputPositionsVarying[11];"
"in vec3 phaseAndAmplitudeVarying;"
"out vec3 fragColour;"
"uniform usampler2D texID;"
<< rgb_shader <<
"void main(void)"
"{"
"fragColour = rgb_sample(texID, inputPositionsVarying[5]);"
"}";
return make_shader(fragment_shader.str(), true, true);
}
std::unique_ptr<IntermediateShader> IntermediateShader::make_chroma_luma_separation_shader() {
return make_shader(
"#version 150\n"
"in vec3 phaseAndAmplitudeVarying;"
"in vec2 inputPositionsVarying[11];"
"out vec3 fragColour;"
"uniform sampler2D texID;"
"void main(void)"
"{"
"vec4 samples = vec4("
"texture(texID, inputPositionsVarying[3]).r,"
"texture(texID, inputPositionsVarying[4]).r,"
"texture(texID, inputPositionsVarying[5]).r,"
"texture(texID, inputPositionsVarying[6]).r"
");"
// calculate luminance as either the straight average of the samples, if a colour subcarrier
// was present, or else a weighted sample around the third sample if not.
"float luminance = mix(dot(samples, vec4(0.25)), dot(samples, vec4(0.0, 0.16, 0.66, 0.16)), step(phaseAndAmplitudeVarying.z, 0.0));"
// define chroma to be whatever was here, minus luma
"float chrominance = 0.5 * (samples.z - luminance) * phaseAndAmplitudeVarying.z;"
// scale luminance up to the range [0, 1)
"luminance /= (1.0 - abs(phaseAndAmplitudeVarying.y));"
// split choma colours here, as the most direct place, writing out
// RGB = (luma, chroma.x, chroma.y)
"vec2 quadrature = vec2(cos(phaseAndAmplitudeVarying.x), sin(phaseAndAmplitudeVarying.x)) * vec2(1.0, sign(phaseAndAmplitudeVarying.y));"
"fragColour = vec3(luminance, vec2(0.5) + (chrominance * quadrature));"
"}",false, false);
}
std::unique_ptr<IntermediateShader> IntermediateShader::make_chroma_filter_shader() {
return make_shader(
"#version 150\n"
"in vec2 inputPositionsVarying[11];"
"uniform vec4 weights[3];"
"out vec3 fragColour;"
"uniform sampler2D texID;"
"uniform mat3 lumaChromaToRGB;"
"void main(void)"
"{"
"vec3 samples[] = vec3[]("
"texture(texID, inputPositionsVarying[3]).rgb,"
"texture(texID, inputPositionsVarying[4]).rgb,"
"texture(texID, inputPositionsVarying[5]).rgb,"
"texture(texID, inputPositionsVarying[6]).rgb"
");"
"vec4 chromaChannel1 = vec4(samples[0].g, samples[1].g, samples[2].g, samples[3].g);"
"vec4 chromaChannel2 = vec4(samples[0].b, samples[1].b, samples[2].b, samples[3].b);"
"vec3 lumaChromaColour = vec3(samples[2].r,"
"dot(chromaChannel1, vec4(0.25)),"
"dot(chromaChannel2, vec4(0.25))"
");"
"vec3 lumaChromaColourInRange = (lumaChromaColour - vec3(0.0, 0.5, 0.5)) * vec3(1.0, 2.0, 2.0);"
"fragColour = lumaChromaToRGB * lumaChromaColourInRange;"
"}", false, false);
}
std::unique_ptr<IntermediateShader> IntermediateShader::make_rgb_filter_shader() {
return make_shader(
"#version 150\n"
"in vec2 inputPositionsVarying[11];"
"uniform vec4 weights[3];"
"out vec3 fragColour;"
"uniform sampler2D texID;"
"void main(void)"
"{"
"vec3 samples[] = vec3[]("
"texture(texID, inputPositionsVarying[0]).rgb,"
"texture(texID, inputPositionsVarying[1]).rgb,"
"texture(texID, inputPositionsVarying[2]).rgb,"
"texture(texID, inputPositionsVarying[3]).rgb,"
"texture(texID, inputPositionsVarying[4]).rgb,"
"texture(texID, inputPositionsVarying[5]).rgb,"
"texture(texID, inputPositionsVarying[6]).rgb,"
"texture(texID, inputPositionsVarying[7]).rgb,"
"texture(texID, inputPositionsVarying[8]).rgb,"
"texture(texID, inputPositionsVarying[9]).rgb,"
"texture(texID, inputPositionsVarying[10]).rgb"
");"
"vec4 channel1[] = vec4[]("
"vec4(samples[0].r, samples[1].r, samples[2].r, samples[3].r),"
"vec4(samples[4].r, samples[5].r, samples[6].r, samples[7].r),"
"vec4(samples[8].r, samples[9].r, samples[10].r, 0.0)"
");"
"vec4 channel2[] = vec4[]("
"vec4(samples[0].g, samples[1].g, samples[2].g, samples[3].g),"
"vec4(samples[4].g, samples[5].g, samples[6].g, samples[7].g),"
"vec4(samples[8].g, samples[9].g, samples[10].g, 0.0)"
");"
"vec4 channel3[] = vec4[]("
"vec4(samples[0].b, samples[1].b, samples[2].b, samples[3].b),"
"vec4(samples[4].b, samples[5].b, samples[6].b, samples[7].b),"
"vec4(samples[8].b, samples[9].b, samples[10].b, 0.0)"
");"
"fragColour = vec3("
"dot(vec3("
"dot(channel1[0], weights[0]),"
"dot(channel1[1], weights[1]),"
"dot(channel1[2], weights[2])"
"), vec3(1.0)),"
"dot(vec3("
"dot(channel2[0], weights[0]),"
"dot(channel2[1], weights[1]),"
"dot(channel2[2], weights[2])"
"), vec3(1.0)),"
"dot(vec3("
"dot(channel3[0], weights[0]),"
"dot(channel3[1], weights[1]),"
"dot(channel3[2], weights[2])"
"), vec3(1.0))"
");"
"}", false, false);
}
void IntermediateShader::set_output_size(unsigned int output_width, unsigned int output_height) {
set_uniform("outputTextureSize", (GLint)output_width, (GLint)output_height);
}
void IntermediateShader::set_source_texture_unit(GLenum unit) {
set_uniform("texID", (GLint)(unit - GL_TEXTURE0));
}
void IntermediateShader::set_filter_coefficients(float sampling_rate, float cutoff_frequency) {
// The process below: the source texture will have bilinear filtering enabled; so by
// sampling at non-integral offsets from the centre the shader can get a weighted sum
// of two source pixels, then scale that once, to do two taps per sample. However
// that works only if the two coefficients being joined have the same sign. So the
// number of usable taps is between 11 and 21 depending on the values that come out.
// Perform a linear search for the highest number of taps we can use with 11 samples.
GLfloat weights[12];
GLfloat offsets[5];
unsigned int taps = 11;
// unsigned int taps = 21;
// while(1) {
// float coefficients[21];
SignalProcessing::FIRFilter luminance_filter(taps, sampling_rate, 0.0f, cutoff_frequency, SignalProcessing::FIRFilter::DefaultAttenuation);
std::vector<float> coefficients = luminance_filter.get_coefficients();
// int sample = 0;
// int c = 0;
std::memset(weights, 0, sizeof(float)*12);
std::memset(offsets, 0, sizeof(float)*5);
unsigned int half_size = (taps >> 1);
for(unsigned int c = 0; c < taps; c++) {
if(c < 5) offsets[c] = (half_size - c);
weights[c] = coefficients[c];
}
// break;
// int halfSize = (taps >> 1);
// while(c < halfSize && sample < 5) {
// offsets[sample] = static_cast<float>(halfSize - c);
// if((coefficients[c] < 0.0f) == (coefficients[c+1] < 0.0f) && c+1 < (taps >> 1)) {
// weights[sample] = coefficients[c] + coefficients[c+1];
// offsets[sample] -= (coefficients[c+1] / weights[sample]);
// c += 2;
// } else {
// weights[sample] = coefficients[c];
// c++;
// }
// sample ++;
// }
// if(c == halfSize) { // i.e. we finished combining inputs before we ran out of space
// weights[sample] = coefficients[c];
// for(int c = 0; c < sample; c++) {
// weights[sample+c+1] = weights[sample-c-1];
// }
// break;
// }
// taps -= 2;
// }
set_uniform("weights", 4, 3, weights);
set_uniform("offsets", 1, 5, offsets);
}
void IntermediateShader::set_separation_frequency(float sampling_rate, float colour_burst_frequency) {
set_filter_coefficients(sampling_rate, colour_burst_frequency);
}
void IntermediateShader::set_extension(float extension) {
set_uniform("extension", extension);
}
void IntermediateShader::set_colour_conversion_matrices(float *fromRGB, float *toRGB) {
set_uniform_matrix("lumaChromaToRGB", 3, false, toRGB);
set_uniform_matrix("rgbToLumaChroma", 3, false, fromRGB);
}
void IntermediateShader::set_width_scalers(float input_scaler, float output_scaler) {
set_uniform("widthScalers", input_scaler, output_scaler);
}
void IntermediateShader::set_is_double_height(bool is_double_height, float input_offset, float output_offset) {
set_uniform("textureHeightDivisor", is_double_height ? 2.0f : 1.0f);
set_uniform("inputVerticalOffset", input_offset);
set_uniform("outputVerticalOffset", output_offset);
}

View File

@ -1,144 +0,0 @@
//
// IntermediateShader.hpp
// Clock Signal
//
// Created by Thomas Harte on 28/04/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#ifndef IntermediateShader_hpp
#define IntermediateShader_hpp
#include "Shader.hpp"
#include <cstdio>
#include <memory>
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,
/// Contains the 2d start position of this run's output position.
OutputStart,
/// A 2d vector comprised of (the final x position for input, the final x position for output).
Ends,
/// A 3d vector recording the colour subcarrier's (phase, time, amplitude) at the start of this span of data.
PhaseTimeAndAmplitude
};
/*!
Obtains the name of a designated input. Designated inputs are guaranteed to have the same attribute location
across multiple instances of IntermediateShader. So binding a vertex array to these inputs for any instance of
IntermediateShader allows that array to work with all instances of IntermediateShader.
@param input The input to query.
@returns The name used in this shader's source for the nominated input.
*/
static std::string get_input_name(Input input);
/*!
Constructs and returns an intermediate shader that will take runs from the inputPositions,
converting them to single-channel composite values using @c composite_shader if non-empty
or a reference composite conversion of @c svideo_shader (first preference) or
@c rgb_shader (second preference) otherwise.
[input format] => one-channel composite.
*/
static std::unique_ptr<IntermediateShader> make_composite_source_shader(const std::string &composite_shader, const std::string &svideo_shader, const std::string &rgb_shader);
/*!
Constructs and returns an intermediate shader that will take runs from the inputPositions,
converting them to two-channel svideo values using @c svideo_shader if non-empty
or a reference svideo conversion of @c rgb_shader otherwise.
[input format] => three-channel Y, noisy (m, n).
*/
static std::unique_ptr<IntermediateShader> make_svideo_source_shader(const std::string &svideo_shader, const std::string &rgb_shader);
/*!
Constructs and returns an intermediate shader that will take runs from the inputPositions,
converting them to RGB values using @c rgb_shader.
[input format] => three-channel RGB.
*/
static std::unique_ptr<IntermediateShader> make_rgb_source_shader(const std::string &rgb_shader);
/*!
Constructs and returns an intermediate shader that will read composite samples from the R channel,
filter then to obtain luminance, stored to R, and to separate out unfiltered chrominance, store to G and B.
one-channel composite => three-channel Y, noisy (m, n).
*/
static std::unique_ptr<IntermediateShader> make_chroma_luma_separation_shader();
/*!
Constructs and returns an intermediate shader that will pass R through unchanged while filtering G and B.
three-channel Y, noisy (m, n) => three-channel RGB.
*/
static std::unique_ptr<IntermediateShader> make_chroma_filter_shader();
/*!
Constructs and returns an intermediate shader that will filter R, G and B.
three-channel RGB => frequency-limited three-channel RGB.
*/
static std::unique_ptr<IntermediateShader> make_rgb_filter_shader();
/*!
Queues the configuration of this shader for output to an area of `output_width` and `output_height` pixels
to occur upon the next `bind`.
*/
void set_output_size(unsigned int output_width, unsigned int output_height);
/*!
Queues setting the texture unit (as an enum, e.g. `GL_TEXTURE0`) for source data to occur upon the next `bind`.
*/
void set_source_texture_unit(GLenum unit);
/*!
Queues setting filtering coefficients for a lowpass filter based on the cutoff frequency to occur upon the next `bind`.
*/
void set_filter_coefficients(float sampling_rate, float cutoff_frequency);
/*!
Queues configuration of filtering to separate luminance and chrominance based on a colour
subcarrier of the given frequency to occur upon the next `bind`.
*/
void set_separation_frequency(float sampling_rate, float colour_burst_frequency);
/*!
Queues setting of the number of colour phase cycles per sample, indicating whether output
geometry should be extended so that a complete colour cycle is included at both the beginning and end,
to occur upon the next `bind`.
*/
void set_extension(float extension);
/*!
Queues setting the matrices that convert between RGB and chrominance/luminance to occur on the next `bind`.
*/
void set_colour_conversion_matrices(float *fromRGB, float *toRGB);
/*!
Sets the proportions of the input and output areas that should be considered the whole width: 1.0 means use all available
space, 0.5 means use half, etc.
*/
void set_width_scalers(float input_scaler, float output_scaler);
/*!
Sets source and target vertical offsets.
*/
void set_is_double_height(bool is_double_height, float input_offset = 0.0f, float output_offset = 0.0f);
private:
static std::unique_ptr<IntermediateShader> make_shader(const std::string &fragment_shader, bool use_usampler, bool input_is_inputPosition);
};
}
#endif /* IntermediateShader_hpp */

View File

@ -1,142 +0,0 @@
//
// OutputShader.cpp
// Clock Signal
//
// Created by Thomas Harte on 27/04/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#include "OutputShader.hpp"
#include <cassert>
#include <cmath>
#include <sstream>
using namespace OpenGL;
std::string OutputShader::get_input_name(Input input) {
switch(input) {
case Input::Horizontal: return "horizontal";
case Input::Vertical: return "vertical";
// Intended to be unreachable.
default: assert(false); return "";
}
}
std::unique_ptr<OutputShader> OutputShader::make_shader(const char *fragment_methods, const char *colour_expression, bool use_usampler) {
const std::string sampler_type = use_usampler ? "usampler2D" : "sampler2D";
std::ostringstream vertex_shader;
vertex_shader <<
"#version 150\n"
"in vec2 " << get_input_name(Input::Horizontal) << ";"
"in vec2 " << get_input_name(Input::Vertical) << ";"
"uniform vec2 boundsOrigin;"
"uniform vec2 boundsSize;"
"uniform vec2 positionConversion;"
"uniform vec2 scanNormal;"
"uniform " << sampler_type << " texID;"
"uniform float inputScaler;"
"uniform int textureHeightDivisor;"
"out float lateralVarying;"
"out vec2 srcCoordinatesVarying;"
"out vec2 iSrcCoordinatesVarying;"
"void main(void)"
"{"
"float lateral = float(gl_VertexID & 1);"
"float longitudinal = float((gl_VertexID & 2) >> 1);"
"float x = mix(horizontal.x, horizontal.y, longitudinal);"
"lateralVarying = lateral - 0.5;"
"vec2 vSrcCoordinates = vec2(x, vertical.y);"
"ivec2 textureSize = textureSize(texID, 0) * ivec2(1, textureHeightDivisor);"
"iSrcCoordinatesVarying = vSrcCoordinates;"
"srcCoordinatesVarying = vec2(inputScaler * vSrcCoordinates.x / textureSize.x, (vSrcCoordinates.y + 0.5) / textureSize.y);"
"srcCoordinatesVarying.x = srcCoordinatesVarying.x - mod(srcCoordinatesVarying.x, 1.0 / textureSize.x);"
"vec2 vPosition = vec2(x, vertical.x);"
"vec2 floatingPosition = (vPosition / positionConversion) + lateral * scanNormal;"
"vec2 mappedPosition = (floatingPosition - boundsOrigin) / boundsSize;"
"gl_Position = vec4(mappedPosition.x * 2.0 - 1.0, 1.0 - mappedPosition.y * 2.0, 0.0, 1.0);"
"}";
std::ostringstream fragment_shader;
fragment_shader <<
"#version 150\n"
"in float lateralVarying;"
"in vec2 srcCoordinatesVarying;"
"in vec2 iSrcCoordinatesVarying;"
"out vec4 fragColour;"
"uniform " << sampler_type << " texID;"
"uniform float gamma;"
<< fragment_methods <<
"void main(void)"
"{"
"fragColour = vec4(pow(" << colour_expression << ", vec3(gamma)), 0.64);"//*cos(lateralVarying)
"}";
return std::unique_ptr<OutputShader>(new OutputShader(vertex_shader.str(), fragment_shader.str(), {
{get_input_name(Input::Horizontal), 0},
{get_input_name(Input::Vertical), 1}
}));
}
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;
left_extent_ = (-1.0f / outputAspectRatioMultiplier) / 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_left_extent() {
return left_extent_;
}
float OutputShader::get_right_extent() {
return right_extent_;
}
void OutputShader::set_source_texture_unit(GLenum unit) {
set_uniform("texID", (GLint)(unit - GL_TEXTURE0));
}
void OutputShader::set_timing(unsigned int height_of_display, unsigned int cycles_per_line, unsigned int horizontal_scan_period, unsigned int vertical_scan_period, unsigned int vertical_period_divider) {
GLfloat scan_angle = atan2f(1.0f / static_cast<float>(height_of_display), 1.0f);
GLfloat scan_normal[] = { -sinf(scan_angle), cosf(scan_angle)};
GLfloat multiplier = static_cast<float>(cycles_per_line) / (static_cast<float>(height_of_display) * static_cast<float>(horizontal_scan_period));
scan_normal[0] *= multiplier;
scan_normal[1] *= multiplier;
set_uniform("scanNormal", scan_normal[0], scan_normal[1]);
set_uniform("positionConversion", (GLfloat)horizontal_scan_period, (GLfloat)vertical_scan_period / (GLfloat)vertical_period_divider);
}
void OutputShader::set_gamma_ratio(float ratio) {
set_uniform("gamma", ratio);
}
void OutputShader::set_input_width_scaler(float input_scaler) {
set_uniform("inputScaler", input_scaler);
}
void OutputShader::set_origin_is_double_height(bool is_double_height) {
set_uniform("textureHeightDivisor", is_double_height ? 2 : 1);
}

View File

@ -1,108 +0,0 @@
//
// OutputShader.hpp
// Clock Signal
//
// Created by Thomas Harte on 27/04/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#ifndef OutputShader_hpp
#define OutputShader_hpp
#include "Shader.hpp"
#include "../../CRTTypes.hpp"
#include <memory>
namespace OpenGL {
class OutputShader: public Shader {
public:
enum class Input {
/// A 2d vector; the first element is the horizontal start of this scan, the second element is the end.
Horizontal,
/// A 2d vector; the first element is the vertical start of this scan, the second element is the end.
Vertical
};
/*!
Obtains the name of a designated input. Designated inputs are guaranteed to have the same attribute location
across multiple instances of OutputShader. So binding a vertex array to these inputs for any instance of
OutputShader allows that array to work with all instances of OutputShader.
@param input The input to query.
@returns The name used in this shader's source for the nominated input.
*/
static std::string get_input_name(Input input);
/*!
Constructs and returns an instance of OutputShader. OutputShaders are intended to read source data
from a texture and draw a single raster scan containing that data as output.
Does not catch any of the exceptions potentially thrown by `Shader::Shader`.
All instances of OutputShader are guaranteed to use the same attribute locations for their inputs.
@param fragment_methods A block of code that will appear within the global area of the fragment shader.
@param colour_expression An expression that should evaluate to a `vec3` indicating the colour at the current location. The
decision should be a function of the uniform `texID`, which will be either a `usampler2D` or a `sampler2D` as per the
`use_usampler` parameter, and the inputs `srcCoordinatesVarying` which is a location within the texture from which to
take the source value, and `iSrcCoordinatesVarying` which is a value proportional to `srcCoordinatesVarying` but scaled
so that one unit equals one source sample.
@param use_usampler Dictates the type of the `texID` uniform; will be a `usampler2D` if this parameter is `true`, a
`sampler2D` otherwise.
@returns an instance of OutputShader.
*/
static std::unique_ptr<OutputShader> make_shader(const char *fragment_methods, const char *colour_expression, bool use_usampler);
using Shader::Shader;
/*!
Queues configuration for output to an area of `output_width` and `output_height` pixels, ensuring
the largest possible drawing size that allows everything within `visible_area` to be visible, to
occur upon the next `bind`.
*/
void set_output_size(unsigned int output_width, unsigned int output_height, Outputs::CRT::Rect visible_area);
/*!
Queues setting of the texture unit (as an enum, e.g. `GL_TEXTURE0`) for source data upon the next `bind`.
*/
void set_source_texture_unit(GLenum unit);
/*!
Queues configuring this shader's understanding of how to map from the source vertex stream to screen coordinates,
to occur upon the next `bind`.
*/
void set_timing(unsigned int height_of_display, unsigned int cycles_per_line, unsigned int horizontal_scan_period, unsigned int vertical_scan_period, unsigned int vertical_period_divider);
/*!
*/
void set_origin_is_double_height(bool is_double_height);
void set_gamma_ratio(float ratio);
/*!
Sets the proportion of the input area that should be considered the whole width: 1.0 means use all available
space, 0.5 means use half, etc.
*/
void set_input_width_scaler(float input_scaler);
/*!
@returns The location, in eye coordinates, of the left edge of the output area.
*/
float get_left_extent();
/*!
@returns The location, in eye coordinates, of the right edge of the output area.
*/
float get_right_extent();
private:
float left_extent_ = 0.0f;
float right_extent_ = 0.0f;
};
}
#endif /* OutputShader_hpp */

View File

@ -1,266 +0,0 @@
//
// Shader.cpp
// Clock Signal
//
// Created by Thomas Harte on 07/02/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#include "Shader.hpp"
#include <cstdio>
#include <cstring>
using namespace OpenGL;
namespace {
// The below is disabled because it isn't context/thread-specific. Which makes it
// fairly 'unuseful'.
// Shader *bound_shader = nullptr;
}
GLuint Shader::compile_shader(const std::string &source, GLenum type) {
GLuint shader = glCreateShader(type);
const char *c_str = source.c_str();
glShaderSource(shader, 1, &c_str, NULL);
glCompileShader(shader);
#ifdef DEBUG
GLint isCompiled = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &isCompiled);
if(isCompiled == GL_FALSE) {
GLint logLength;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logLength);
if(logLength > 0) {
GLchar *log = new GLchar[static_cast<std::size_t>(logLength)];
glGetShaderInfoLog(shader, logLength, &logLength, log);
printf("Compile log:\n%s\n", log);
delete[] log;
}
throw (type == GL_VERTEX_SHADER) ? VertexShaderCompilationError : FragmentShaderCompilationError;
}
#endif
return shader;
}
Shader::Shader(const std::string &vertex_shader, const std::string &fragment_shader, const std::vector<AttributeBinding> &attribute_bindings) {
shader_program_ = glCreateProgram();
GLuint vertex = compile_shader(vertex_shader, GL_VERTEX_SHADER);
GLuint fragment = compile_shader(fragment_shader, GL_FRAGMENT_SHADER);
glAttachShader(shader_program_, vertex);
glAttachShader(shader_program_, fragment);
for(const auto &binding : attribute_bindings) {
glBindAttribLocation(shader_program_, binding.index, binding.name.c_str());
}
glLinkProgram(shader_program_);
#ifdef DEBUG
GLint didLink = 0;
glGetProgramiv(shader_program_, GL_LINK_STATUS, &didLink);
if(didLink == GL_FALSE) {
GLint logLength;
glGetProgramiv(shader_program_, GL_INFO_LOG_LENGTH, &logLength);
if(logLength > 0) {
GLchar *log = new GLchar[static_cast<std::size_t>(logLength)];
glGetProgramInfoLog(shader_program_, logLength, &logLength, log);
printf("Link log:\n%s\n", log);
delete[] log;
}
throw ProgramLinkageError;
}
#endif
}
Shader::~Shader() {
// if(bound_shader == this) Shader::unbind();
glDeleteProgram(shader_program_);
}
void Shader::bind() {
// if(bound_shader != this) {
glUseProgram(shader_program_);
// bound_shader = this;
// }
flush_functions();
}
void Shader::unbind() {
// bound_shader = nullptr;
glUseProgram(0);
}
GLint Shader::get_attrib_location(const std::string &name) {
return glGetAttribLocation(shader_program_, name.c_str());
}
GLint Shader::get_uniform_location(const std::string &name) {
return glGetUniformLocation(shader_program_, name.c_str());
}
void Shader::enable_vertex_attribute_with_pointer(const std::string &name, GLint size, GLenum type, GLboolean normalised, GLsizei stride, const GLvoid *pointer, GLuint divisor) {
GLint location = get_attrib_location(name);
glEnableVertexAttribArray((GLuint)location);
glVertexAttribPointer((GLuint)location, size, type, normalised, stride, pointer);
glVertexAttribDivisor((GLuint)location, divisor);
}
// The various set_uniforms...
#define location() glGetUniformLocation(shader_program_, name.c_str())
void Shader::set_uniform(const std::string &name, GLint value) {
enqueue_function([name, value, this] {
glUniform1i(location(), value);
});
}
void Shader::set_uniform(const std::string &name, GLuint value) {
enqueue_function([name, value, this] {
glUniform1ui(location(), value);
});
}
void Shader::set_uniform(const std::string &name, GLfloat value) {
enqueue_function([name, value, this] {
glUniform1f(location(), value);
});
}
void Shader::set_uniform(const std::string &name, GLint value1, GLint value2) {
enqueue_function([name, value1, value2, this] {
glUniform2i(location(), value1, value2);
});
}
void Shader::set_uniform(const std::string &name, GLfloat value1, GLfloat value2) {
enqueue_function([name, value1, value2, this] {
GLint location = location();
glUniform2f(location, value1, value2);
});
}
void Shader::set_uniform(const std::string &name, GLuint value1, GLuint value2) {
enqueue_function([name, value1, value2, this] {
glUniform2ui(location(), value1, value2);
});
}
void Shader::set_uniform(const std::string &name, GLint value1, GLint value2, GLint value3) {
enqueue_function([name, value1, value2, value3, this] {
glUniform3i(location(), value1, value2, value3);
});
}
void Shader::set_uniform(const std::string &name, GLfloat value1, GLfloat value2, GLfloat value3) {
enqueue_function([name, value1, value2, value3, this] {
glUniform3f(location(), value1, value2, value3);
});
}
void Shader::set_uniform(const std::string &name, GLuint value1, GLuint value2, GLuint value3) {
enqueue_function([name, value1, value2, value3, this] {
glUniform3ui(location(), value1, value2, value3);
});
}
void Shader::set_uniform(const std::string &name, GLint value1, GLint value2, GLint value3, GLint value4) {
enqueue_function([name, value1, value2, value3, value4, this] {
glUniform4i(location(), value1, value2, value3, value4);
});
}
void Shader::set_uniform(const std::string &name, GLfloat value1, GLfloat value2, GLfloat value3, GLfloat value4) {
enqueue_function([name, value1, value2, value3, value4, this] {
glUniform4f(location(), value1, value2, value3, value4);
});
}
void Shader::set_uniform(const std::string &name, GLuint value1, GLuint value2, GLuint value3, GLuint value4) {
enqueue_function([name, value1, value2, value3, value4, this] {
glUniform4ui(location(), value1, value2, value3, value4);
});
}
void Shader::set_uniform(const std::string &name, GLint size, GLsizei count, const GLint *values) {
std::size_t number_of_values = static_cast<std::size_t>(count) * static_cast<std::size_t>(size);
GLint *values_copy = new GLint[number_of_values];
std::memcpy(values_copy, values, sizeof(*values) * static_cast<std::size_t>(number_of_values));
enqueue_function([name, size, count, values_copy, this] {
switch(size) {
case 1: glUniform1iv(location(), count, values_copy); break;
case 2: glUniform2iv(location(), count, values_copy); break;
case 3: glUniform3iv(location(), count, values_copy); break;
case 4: glUniform4iv(location(), count, values_copy); break;
}
delete[] values_copy;
});
}
void Shader::set_uniform(const std::string &name, GLint size, GLsizei count, const GLfloat *values) {
std::size_t number_of_values = static_cast<std::size_t>(count) * static_cast<std::size_t>(size);
GLfloat *values_copy = new GLfloat[number_of_values];
std::memcpy(values_copy, values, sizeof(*values) * static_cast<std::size_t>(number_of_values));
enqueue_function([name, size, count, values_copy, this] {
switch(size) {
case 1: glUniform1fv(location(), count, values_copy); break;
case 2: glUniform2fv(location(), count, values_copy); break;
case 3: glUniform3fv(location(), count, values_copy); break;
case 4: glUniform4fv(location(), count, values_copy); break;
}
delete[] values_copy;
});
}
void Shader::set_uniform(const std::string &name, GLint size, GLsizei count, const GLuint *values) {
std::size_t number_of_values = static_cast<std::size_t>(count) * static_cast<std::size_t>(size);
GLuint *values_copy = new GLuint[number_of_values];
std::memcpy(values_copy, values, sizeof(*values) * static_cast<std::size_t>(number_of_values));
enqueue_function([name, size, count, values_copy, this] {
switch(size) {
case 1: glUniform1uiv(location(), count, values_copy); break;
case 2: glUniform2uiv(location(), count, values_copy); break;
case 3: glUniform3uiv(location(), count, values_copy); break;
case 4: glUniform4uiv(location(), count, values_copy); break;
}
delete[] values_copy;
});
}
void Shader::set_uniform_matrix(const std::string &name, GLint size, bool transpose, const GLfloat *values) {
set_uniform_matrix(name, size, 1, transpose, values);
}
void Shader::set_uniform_matrix(const std::string &name, GLint size, GLsizei count, bool transpose, const GLfloat *values) {
std::size_t number_of_values = static_cast<std::size_t>(count) * static_cast<std::size_t>(size) * static_cast<std::size_t>(size);
std::vector<GLfloat> values_copy(number_of_values);
std::memcpy(values_copy.data(), values, sizeof(*values) * number_of_values);
enqueue_function([name, size, count, transpose, values_copy, this] {
GLboolean glTranspose = transpose ? GL_TRUE : GL_FALSE;
switch(size) {
case 2: glUniformMatrix2fv(location(), count, glTranspose, values_copy.data()); break;
case 3: glUniformMatrix3fv(location(), count, glTranspose, values_copy.data()); break;
case 4: glUniformMatrix4fv(location(), count, glTranspose, values_copy.data()); break;
}
});
}
void Shader::enqueue_function(std::function<void(void)> function) {
std::lock_guard<std::mutex> function_guard(function_mutex_);
enqueued_functions_.push_back(function);
}
void Shader::flush_functions() {
std::lock_guard<std::mutex> function_guard(function_mutex_);
for(std::function<void(void)> function : enqueued_functions_) {
function();
}
enqueued_functions_.clear();
}

View File

@ -1,179 +0,0 @@
//
// TextureBuilder.cpp
// Clock Signal
//
// Created by Thomas Harte on 08/03/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#include "TextureBuilder.hpp"
#include "CRTOpenGL.hpp"
#include "OpenGL.hpp"
#include <cstring>
using namespace Outputs::CRT;
namespace {
const GLint internalFormatForDepth(std::size_t depth) {
switch(depth) {
default: return GL_FALSE;
case 1: return GL_R8UI;
case 2: return GL_RG8UI;
case 3: return GL_RGB8UI;
case 4: return GL_RGBA8UI;
}
}
const GLenum formatForDepth(std::size_t depth) {
switch(depth) {
default: return GL_FALSE;
case 1: return GL_RED_INTEGER;
case 2: return GL_RG_INTEGER;
case 3: return GL_RGB_INTEGER;
case 4: return GL_RGBA_INTEGER;
}
}
}
TextureBuilder::TextureBuilder(std::size_t bytes_per_pixel, GLenum texture_unit) :
bytes_per_pixel_(bytes_per_pixel), texture_unit_(texture_unit) {
image_.resize(bytes_per_pixel * InputBufferBuilderWidth * InputBufferBuilderHeight);
glGenTextures(1, &texture_name_);
bind();
glTexImage2D(GL_TEXTURE_2D, 0, internalFormatForDepth(bytes_per_pixel), InputBufferBuilderWidth, InputBufferBuilderHeight, 0, formatForDepth(bytes_per_pixel), GL_UNSIGNED_BYTE, nullptr);
}
TextureBuilder::~TextureBuilder() {
glDeleteTextures(1, &texture_name_);
}
void TextureBuilder::bind() {
glActiveTexture(texture_unit_);
glBindTexture(GL_TEXTURE_2D, texture_name_);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
}
inline uint8_t *TextureBuilder::pointer_to_location(uint16_t x, uint16_t y) {
return &image_[((y * InputBufferBuilderWidth) + x) * bytes_per_pixel_];
}
uint8_t *TextureBuilder::allocate_write_area(std::size_t required_length, std::size_t required_alignment) {
// Keep a flag to indicate whether the buffer was full at allocate_write_area; if it was then
// don't return anything now, and decline to act upon follow-up methods. is_full_ may be reset
// by asynchronous calls to submit. was_full_ will not be touched by it.
was_full_ = is_full_;
if(is_full_) return nullptr;
// If there's not enough space on this line, move to the next. If the next is where the current
// submission group started, trigger is/was_full_ and return nothing.
std::size_t alignment_offset = (required_alignment - ((write_areas_start_x_ + 1) % required_alignment)) % required_alignment;
if(write_areas_start_x_ + required_length + 2 + alignment_offset > InputBufferBuilderWidth) {
write_areas_start_x_ = 0;
alignment_offset = required_alignment - 1;
write_areas_start_y_ = (write_areas_start_y_ + 1) % InputBufferBuilderHeight;
if(write_areas_start_y_ == first_unsubmitted_y_) {
was_full_ = is_full_ = true;
return nullptr;
}
}
// Queue up the latest write area.
write_areas_start_x_ += static_cast<uint16_t>(alignment_offset);
write_area_.x = write_areas_start_x_ + 1;
write_area_.y = write_areas_start_y_;
write_area_.length = static_cast<uint16_t>(required_length);
// Return a video pointer.
return pointer_to_location(write_area_.x, write_area_.y);
}
void TextureBuilder::reduce_previous_allocation_to(std::size_t actual_length) {
// If the previous allocate_write_area declined to act, decline also.
if(was_full_) return;
// Update the length of the current write area.
write_area_.length = static_cast<uint16_t>(actual_length);
// Bookend the allocation with duplicates of the first and last pixel, to protect
// against rounding errors when this run is drawn.
uint8_t *start_pointer = pointer_to_location(write_area_.x, write_area_.y) - bytes_per_pixel_;
std::memcpy(start_pointer, &start_pointer[bytes_per_pixel_], bytes_per_pixel_);
std::memcpy(&start_pointer[(actual_length + 1) * bytes_per_pixel_], &start_pointer[actual_length * bytes_per_pixel_], bytes_per_pixel_);
}
bool TextureBuilder::retain_latest() {
// If the previous allocate_write_area declined to act, decline also.
if(was_full_) return false;
// Account for the most recently written area as taken.
write_areas_start_x_ += write_area_.length + 2;
// Store into the vector directly if there's already room, otherwise grow the vector.
// Probably I don't need to mess about with this myself; it's unnecessary second-guessing.
// TODO: profile and prove.
if(number_of_write_areas_ < write_areas_.size())
write_areas_[number_of_write_areas_] = write_area_;
else
write_areas_.push_back(write_area_);
number_of_write_areas_++;
return true;
}
void TextureBuilder::discard_latest() {
if(was_full_) return;
number_of_write_areas_--;
}
bool TextureBuilder::is_full() {
return is_full_;
}
void TextureBuilder::submit() {
if(write_areas_start_y_ < first_unsubmitted_y_) {
// A write area start y less than the first line on which submissions began implies it must have wrapped
// around. So the submission set is everything back to zero before the current write area plus everything
// from the first unsubmitted y downward.
uint16_t height = write_areas_start_y_ + (write_areas_start_x_ ? 1 : 0);
glTexSubImage2D( GL_TEXTURE_2D, 0,
0, 0,
InputBufferBuilderWidth, height,
formatForDepth(bytes_per_pixel_), GL_UNSIGNED_BYTE,
image_.data());
glTexSubImage2D( GL_TEXTURE_2D, 0,
0, first_unsubmitted_y_,
InputBufferBuilderWidth, InputBufferBuilderHeight - first_unsubmitted_y_,
formatForDepth(bytes_per_pixel_), GL_UNSIGNED_BYTE,
image_.data() + first_unsubmitted_y_ * bytes_per_pixel_ * InputBufferBuilderWidth);
} else {
// If the current write area start y is after the first unsubmitted line, just submit the region in between.
uint16_t height = write_areas_start_y_ + (write_areas_start_x_ ? 1 : 0) - first_unsubmitted_y_;
glTexSubImage2D( GL_TEXTURE_2D, 0,
0, first_unsubmitted_y_,
InputBufferBuilderWidth, height,
formatForDepth(bytes_per_pixel_), GL_UNSIGNED_BYTE,
image_.data() + first_unsubmitted_y_ * bytes_per_pixel_ * InputBufferBuilderWidth);
}
// Update the starting location for the next submission, and mark definitively that the buffer is once again not full.
first_unsubmitted_y_ = write_areas_start_y_;
is_full_ = false;
}
void TextureBuilder::flush(const std::function<void(const std::vector<WriteArea> &write_areas, std::size_t count)> &function) {
// Just throw everything currently in the flush queue to the provided function, and note that
// the queue is now empty.
if(number_of_write_areas_) {
function(write_areas_, number_of_write_areas_);
}
number_of_write_areas_ = 0;
}

View File

@ -1,132 +0,0 @@
//
// TextureBuilder.hpp
// Clock Signal
//
// Created by Thomas Harte on 08/03/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#ifndef Outputs_CRT_Internals_TextureBuilder_hpp
#define Outputs_CRT_Internals_TextureBuilder_hpp
#include <cstdint>
#include <functional>
#include <memory>
#include <vector>
#include "OpenGL.hpp"
#include "CRTConstants.hpp"
namespace Outputs {
namespace CRT {
/*!
Owns an OpenGL texture resource and provides mechanisms to fill it from bottom left to top right
with runs of data, ensuring each run is neighboured immediately to the left and right by copies of its
first and last pixels.
Although this class is not itself inherently thread safe, it is built to permit one serialised stream
of calls to provide source data, with an interceding (but also serialised) submission to the GPU at any time.
Intended usage by the data generator:
(i) allocate a write area with allocate_write_area, supplying a maximum size.
(ii) call reduce_previous_allocation_to to announce the actual size written.
This will cause you to have added source data to the target texture. You can then either use that data
or allow it to expire.
(iii) call retain_latest to add the most recently written write area to the flush queue.
The flush queue contains provisional data, that can sit in the CPU's memory space indefinitely. This facility
is provided because it is expected that a texture will be built alontside some other collection of data;
that data in the flush queue is expected to become useful in coordination with something else but should
be retained at least until then.
(iv) call flush to move data to the submit queue.
When you flush, you'll receive a record of the bounds of all newly-flushed areas of source data. That gives
an opportunity to correlate the data with whatever else it is being tied to. It will continue to sit in
the CPU's memory space but has now passed beyond any further modification or reporting.
Intended usage by the GPU owner:
(i) call submit to move data to the GPU and free up its CPU-side resources.
The latest data is now on the GPU, regardless of where the data provider may be in its process; only data
that has entered the submission queue is uploaded.
*/
class TextureBuilder {
public:
/// Constructs an instance of InputTextureBuilder that contains a texture of colour depth @c bytes_per_pixel;
/// this creates a new texture and binds it to the current active texture unit.
TextureBuilder(std::size_t bytes_per_pixel, GLenum texture_unit);
virtual ~TextureBuilder();
/// Finds the first available space of at least @c required_length pixels in size which is suitably aligned
/// for writing of @c required_alignment number of pixels at a time.
/// Calls must be paired off with calls to @c reduce_previous_allocation_to.
/// @returns a pointer to the allocated space if any was available; @c nullptr otherwise.
uint8_t *allocate_write_area(std::size_t required_length, std::size_t required_alignment = 1);
/// Announces that the owner is finished with the region created by the most recent @c allocate_write_area
/// and indicates that its actual final size was @c actual_length.
void reduce_previous_allocation_to(std::size_t actual_length);
/// Allocated runs are provisional; they will not appear in the next flush queue unless retained.
/// @returns @c true if a retain succeeded; @c false otherwise.
bool retain_latest();
// Undoes the most recent retain_latest. Undefined behaviour if a submission has occurred in the interim.
void discard_latest();
/// @returns @c true if all future calls to @c allocate_write_area will fail on account of the input texture
/// being full; @c false if calls may succeed.
bool is_full();
/// Updates the currently-bound texture with all new data provided since the last @c submit.
void submit();
struct WriteArea {
uint16_t x = 0, y = 0, length = 0;
};
/// Finalises all write areas allocated since the last call to @c flush. Only finalised areas will be
/// submitted upon the next @c submit. The supplied function will be called with a list of write areas
/// allocated, indicating their final resting locations and their lengths.
void flush(const std::function<void(const std::vector<WriteArea> &write_areas, std::size_t count)> &);
/// Binds this texture to the unit supplied at instantiation.
void bind();
private:
// the buffer size and target unit
std::size_t bytes_per_pixel_;
GLenum texture_unit_;
// the buffer
std::vector<uint8_t> image_;
GLuint texture_name_;
// the current write area
WriteArea write_area_;
// the list of write areas that have ascended to the flush queue
std::vector<WriteArea> write_areas_;
std::size_t number_of_write_areas_ = 0;
bool is_full_ = false, was_full_ = false;
uint16_t first_unsubmitted_y_ = 0;
inline uint8_t *pointer_to_location(uint16_t x, uint16_t y);
// Usually: the start position for the current batch of write areas.
// Caveat: reset to the origin upon a submit. So used in comparison by flush to
// determine whether the current batch of write areas needs to be relocated.
uint16_t write_areas_start_x_ = 0, write_areas_start_y_ = 0;
};
}
}
#endif /* Outputs_CRT_Internals_TextureBuilder_hpp */

View File

@ -1,146 +0,0 @@
//
// TextureTarget.cpp
// Clock Signal
//
// Created by Thomas Harte on 07/02/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#include "TextureTarget.hpp"
#include <cstdlib>
#include <vector>
using namespace OpenGL;
TextureTarget::TextureTarget(GLsizei width, GLsizei height, GLenum texture_unit, GLint mag_filter) :
width_(width),
height_(height),
texture_unit_(texture_unit) {
glGenFramebuffers(1, &framebuffer_);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_);
expanded_width_ = 1;
while(expanded_width_ < width) expanded_width_ <<= 1;
expanded_height_ = 1;
while(expanded_height_ < height) expanded_height_ <<= 1;
glGenTextures(1, &texture_);
bind_texture();
std::vector<uint8_t> blank_buffer(static_cast<size_t>(expanded_width_ * expanded_height_ * 4), 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, static_cast<GLsizei>(expanded_width_), static_cast<GLsizei>(expanded_height_), 0, GL_RGBA, GL_UNSIGNED_BYTE, blank_buffer.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture_, 0);
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
throw ErrorFramebufferIncomplete;
}
TextureTarget::~TextureTarget() {
glDeleteFramebuffers(1, &framebuffer_);
glDeleteTextures(1, &texture_);
if(drawing_vertex_array_) glDeleteVertexArrays(1, &drawing_vertex_array_);
if(drawing_array_buffer_) glDeleteBuffers(1, &drawing_array_buffer_);
}
void TextureTarget::bind_framebuffer() {
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer_);
glViewport(0, 0, width_, height_);
}
void TextureTarget::bind_texture() {
glActiveTexture(texture_unit_);
glBindTexture(GL_TEXTURE_2D, texture_);
}
void TextureTarget::draw(float aspect_ratio, float colour_threshold) {
if(!pixel_shader_) {
const char *vertex_shader =
"#version 150\n"
"in vec2 texCoord;"
"in vec2 position;"
"out vec2 texCoordVarying;"
"void main(void)"
"{"
"texCoordVarying = texCoord;"
"gl_Position = vec4(position, 0.0, 1.0);"
"}";
const char *fragment_shader =
"#version 150\n"
"in vec2 texCoordVarying;"
"uniform sampler2D texID;"
"uniform float threshold;"
"out vec4 fragColour;"
"void main(void)"
"{"
"fragColour = clamp(texture(texID, texCoordVarying), threshold, 1.0);"
"}";
pixel_shader_.reset(new Shader(vertex_shader, fragment_shader));
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");
GLint tex_coord_attribute = pixel_shader_->get_attrib_location("texCoord");
glEnableVertexAttribArray(static_cast<GLuint>(position_attribute));
glEnableVertexAttribArray(static_cast<GLuint>(tex_coord_attribute));
const GLsizei vertex_stride = 4 * sizeof(GLfloat);
glVertexAttribPointer((GLuint)position_attribute, 2, GL_FLOAT, GL_FALSE, vertex_stride, (void *)0);
glVertexAttribPointer((GLuint)tex_coord_attribute, 2, GL_FLOAT, GL_FALSE, vertex_stride, (void *)(2 * sizeof(GLfloat)));
GLint texIDUniform = pixel_shader_->get_uniform_location("texID");
glUniform1i(texIDUniform, static_cast<GLint>(texture_unit_ - GL_TEXTURE0));
threshold_uniform_ = pixel_shader_->get_uniform_location("threshold");
}
if(set_aspect_ratio_ != aspect_ratio) {
set_aspect_ratio_ = aspect_ratio;
float buffer[4*4];
// establish texture coordinates
buffer[2] = 0.0f;
buffer[3] = 0.0f;
buffer[6] = 0.0f;
buffer[7] = static_cast<float>(height_) / static_cast<float>(expanded_height_);
buffer[10] = static_cast<float>(width_) / static_cast<float>(expanded_width_);
buffer[11] = 0.0f;
buffer[14] = buffer[10];
buffer[15] = buffer[7];
// determine positions; rule is to keep the same height and centre
float internal_aspect_ratio = static_cast<float>(width_) / static_cast<float>(height_);
float aspect_ratio_ratio = internal_aspect_ratio / aspect_ratio;
buffer[0] = -aspect_ratio_ratio; buffer[1] = -1.0f;
buffer[4] = -aspect_ratio_ratio; buffer[5] = 1.0f;
buffer[8] = aspect_ratio_ratio; buffer[9] = -1.0f;
buffer[12] = aspect_ratio_ratio; buffer[13] = 1.0f;
// upload buffer
glBindBuffer(GL_ARRAY_BUFFER, drawing_array_buffer_);
glBufferData(GL_ARRAY_BUFFER, sizeof(buffer), buffer, GL_STATIC_DRAW);
}
pixel_shader_->bind();
glUniform1f(threshold_uniform_, colour_threshold);
glBindVertexArray(drawing_vertex_array_);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}

57
Outputs/OpenGL/OpenGL.hpp Normal file
View File

@ -0,0 +1,57 @@
//
// OpenGL.h
// Clock Signal
//
// Created by Thomas Harte on 07/02/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#ifndef OpenGL_h
#define OpenGL_h
#include <cassert>
#include <iostream>
// TODO: figure out correct include paths for other platforms.
#ifdef __APPLE__
#if TARGET_OS_IPHONE
#else
#include <OpenGL/OpenGL.h>
#include <OpenGL/gl3.h>
#include <OpenGL/gl3ext.h>
#endif
#else
#define GL_GLEXT_PROTOTYPES
#include <GL/gl.h>
#endif
#ifndef NDEBUG
#define test_gl_error() { \
const auto error = glGetError(); \
if(error) { \
switch(error) { \
default: std::cerr << "Error " << error; break; \
case GL_INVALID_ENUM: std::cerr << "GL_INVALID_ENUM"; break; \
case GL_INVALID_VALUE: std::cerr << "GL_INVALID_VALUE"; break; \
case GL_INVALID_OPERATION: std::cerr << "GL_INVALID_OPERATION"; break; \
case GL_INVALID_FRAMEBUFFER_OPERATION: std::cerr << "GL_INVALID_FRAMEBUFFER_OPERATION"; break; \
case GL_OUT_OF_MEMORY: std::cerr << "GL_OUT_OF_MEMORY"; break; \
}; \
std::cerr << " at line " << __LINE__ << " in " << __FILE__ << std::endl; \
assert(false); \
} \
\
}
#else
#define test_gl_error() while(false) {}
#endif
#ifndef NDEBUG
#define test_gl(command, ...) do { command(__VA_ARGS__); test_gl_error(); } while(false);
#else
#define test_gl(command, ...) command(__VA_ARGS__)
#endif
#endif /* OpenGL_h */

View File

@ -8,10 +8,10 @@
#include "Rectangle.hpp"
using namespace OpenGL;
using namespace Outputs::Display::OpenGL;
Rectangle::Rectangle(float x, float y, float width, float height):
pixel_shader_(
pixel_shader_(
"#version 150\n"
"in vec2 position;"
@ -33,16 +33,16 @@ Rectangle::Rectangle(float x, float y, float width, float height):
){
pixel_shader_.bind();
glGenVertexArrays(1, &drawing_vertex_array_);
glGenBuffers(1, &drawing_array_buffer_);
test_gl(glGenVertexArrays, 1, &drawing_vertex_array_);
test_gl(glGenBuffers, 1, &drawing_array_buffer_);
glBindVertexArray(drawing_vertex_array_);
glBindBuffer(GL_ARRAY_BUFFER, drawing_array_buffer_);
test_gl(glBindVertexArray, drawing_vertex_array_);
test_gl(glBindBuffer, GL_ARRAY_BUFFER, drawing_array_buffer_);
GLint position_attribute = pixel_shader_.get_attrib_location("position");
glEnableVertexAttribArray(static_cast<GLuint>(position_attribute));
test_gl(glEnableVertexAttribArray, GLuint(position_attribute));
glVertexAttribPointer(
test_gl(glVertexAttribPointer,
(GLuint)position_attribute,
2,
GL_FLOAT,
@ -61,14 +61,14 @@ Rectangle::Rectangle(float x, float y, float width, float height):
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);
test_gl(glBindBuffer, GL_ARRAY_BUFFER, drawing_array_buffer_);
test_gl(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);
test_gl(glUniform4f, colour_uniform_, red, green, blue, 1.0);
glBindVertexArray(drawing_vertex_array_);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
test_gl(glBindVertexArray, drawing_vertex_array_);
test_gl(glDrawArrays, GL_TRIANGLE_STRIP, 0, 4);
}

View File

@ -9,10 +9,12 @@
#ifndef Rectangle_hpp
#define Rectangle_hpp
#include "OpenGL.hpp"
#include "Shaders/Shader.hpp"
#include "../OpenGL.hpp"
#include "Shader.hpp"
#include <memory>
namespace Outputs {
namespace Display {
namespace OpenGL {
/*!
@ -36,6 +38,8 @@ class Rectangle {
GLint colour_uniform_;
};
}
}
}
#endif /* Rectangle_hpp */

View File

@ -0,0 +1,301 @@
//
// Shader.cpp
// Clock Signal
//
// Created by Thomas Harte on 07/02/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#include "Shader.hpp"
#include "../../Log.hpp"
#include <vector>
using namespace Outputs::Display::OpenGL;
namespace {
// The below is disabled because it isn't context/thread-specific. Which makes it
// fairly 'unuseful'.
// Shader *bound_shader = nullptr;
}
GLuint Shader::compile_shader(const std::string &source, GLenum type) {
GLuint shader = glCreateShader(type);
const char *c_str = source.c_str();
test_gl(glShaderSource, shader, 1, &c_str, NULL);
test_gl(glCompileShader, shader);
#ifndef NDEBUG
GLint isCompiled = 0;
test_gl(glGetShaderiv, shader, GL_COMPILE_STATUS, &isCompiled);
if(isCompiled == GL_FALSE) {
GLint logLength;
test_gl(glGetShaderiv, shader, GL_INFO_LOG_LENGTH, &logLength);
if(logLength > 0) {
const auto length = std::vector<GLchar>::size_type(logLength);
std::vector<GLchar> log(length);
test_gl(glGetShaderInfoLog, shader, logLength, &logLength, log.data());
LOG("Compile log:\n" << log.data());
}
throw (type == GL_VERTEX_SHADER) ? VertexShaderCompilationError : FragmentShaderCompilationError;
}
#endif
return shader;
}
Shader::Shader(const std::string &vertex_shader, const std::string &fragment_shader, const std::vector<AttributeBinding> &attribute_bindings) {
init(vertex_shader, fragment_shader, attribute_bindings);
}
Shader::Shader(const std::string &vertex_shader, const std::string &fragment_shader, const std::vector<std::string> &binding_names) {
std::vector<AttributeBinding> bindings;
GLuint index = 0;
for(const auto &name: binding_names) {
bindings.emplace_back(name, index);
++index;
}
init(vertex_shader, fragment_shader, bindings);
}
void Shader::init(const std::string &vertex_shader, const std::string &fragment_shader, const std::vector<AttributeBinding> &attribute_bindings) {
shader_program_ = glCreateProgram();
const GLuint vertex = compile_shader(vertex_shader, GL_VERTEX_SHADER);
const GLuint fragment = compile_shader(fragment_shader, GL_FRAGMENT_SHADER);
test_gl(glAttachShader, shader_program_, vertex);
test_gl(glAttachShader, shader_program_, fragment);
for(const auto &binding : attribute_bindings) {
test_gl(glBindAttribLocation, shader_program_, binding.index, binding.name.c_str());
#ifndef NDEBUG
const auto error = glGetError();
switch(error) {
case 0: break;
case GL_INVALID_VALUE:
LOG("GL_INVALID_VALUE when attempting to bind " << binding.name << " to index " << binding.index << " (i.e. index is greater than or equal to GL_MAX_VERTEX_ATTRIBS)");
break;
case GL_INVALID_OPERATION:
LOG("GL_INVALID_OPERATION when attempting to bind " << binding.name << " to index " << binding.index << " (i.e. name begins with gl_)");
break;
default:
LOG("Error " << error << " when attempting to bind " << binding.name << " to index " << binding.index);
break;
}
#endif
}
test_gl(glLinkProgram, shader_program_);
#ifndef NDEBUG
GLint logLength;
test_gl(glGetProgramiv, shader_program_, GL_INFO_LOG_LENGTH, &logLength);
if(logLength > 0) {
GLchar *log = new GLchar[std::size_t(logLength)];
test_gl(glGetProgramInfoLog, shader_program_, logLength, &logLength, log);
LOG("Link log:\n" << log);
delete[] log;
}
GLint didLink = 0;
test_gl(glGetProgramiv, shader_program_, GL_LINK_STATUS, &didLink);
if(didLink == GL_FALSE) {
throw ProgramLinkageError;
}
#endif
}
Shader::~Shader() {
// if(bound_shader == this) Shader::unbind();
glDeleteProgram(shader_program_);
}
void Shader::bind() const {
// if(bound_shader != this) {
test_gl(glUseProgram, shader_program_);
// bound_shader = this;
// }
flush_functions();
}
void Shader::unbind() {
// bound_shader = nullptr;
test_gl(glUseProgram, 0);
}
GLint Shader::get_attrib_location(const std::string &name) const {
return glGetAttribLocation(shader_program_, name.c_str());
}
GLint Shader::get_uniform_location(const std::string &name) const {
return glGetUniformLocation(shader_program_, name.c_str());
}
void Shader::enable_vertex_attribute_with_pointer(const std::string &name, GLint size, GLenum type, GLboolean normalised, GLsizei stride, const GLvoid *pointer, GLuint divisor) {
GLint location = get_attrib_location(name);
if(location >= 0) {
test_gl(glEnableVertexAttribArray, GLuint(location));
test_gl(glVertexAttribPointer, GLuint(location), size, type, normalised, stride, pointer);
test_gl(glVertexAttribDivisor, GLuint(location), divisor);
} else {
LOG("Couldn't enable vertex attribute " << name);
}
}
// The various set_uniforms...
#define with_location(func, ...) {\
const GLint location = glGetUniformLocation(shader_program_, name.c_str()); \
if(location == -1) { \
LOG("Couldn't get location for uniform " << name); \
} else { \
func(location, __VA_ARGS__); \
if(glGetError()) LOG("Error setting uniform " << name << " via " << #func); \
} \
}
void Shader::set_uniform(const std::string &name, GLint value) {
enqueue_function([name, value, this] {
with_location(glUniform1i, value);
});
}
void Shader::set_uniform(const std::string &name, GLuint value) {
enqueue_function([name, value, this] {
with_location(glUniform1ui, value);
});
}
void Shader::set_uniform(const std::string &name, GLfloat value) {
enqueue_function([name, value, this] {
with_location(glUniform1f, value);
});
}
void Shader::set_uniform(const std::string &name, GLint value1, GLint value2) {
enqueue_function([name, value1, value2, this] {
with_location(glUniform2i, value1, value2);
});
}
void Shader::set_uniform(const std::string &name, GLfloat value1, GLfloat value2) {
enqueue_function([name, value1, value2, this] {
with_location(glUniform2f, value1, value2);
});
}
void Shader::set_uniform(const std::string &name, GLuint value1, GLuint value2) {
enqueue_function([name, value1, value2, this] {
with_location(glUniform2ui, value1, value2);
});
}
void Shader::set_uniform(const std::string &name, GLint value1, GLint value2, GLint value3) {
enqueue_function([name, value1, value2, value3, this] {
with_location(glUniform3i, value1, value2, value3);
});
}
void Shader::set_uniform(const std::string &name, GLfloat value1, GLfloat value2, GLfloat value3) {
enqueue_function([name, value1, value2, value3, this] {
with_location(glUniform3f, value1, value2, value3);
});
}
void Shader::set_uniform(const std::string &name, GLuint value1, GLuint value2, GLuint value3) {
enqueue_function([name, value1, value2, value3, this] {
with_location(glUniform3ui, value1, value2, value3);
});
}
void Shader::set_uniform(const std::string &name, GLint value1, GLint value2, GLint value3, GLint value4) {
enqueue_function([name, value1, value2, value3, value4, this] {
with_location(glUniform4i, value1, value2, value3, value4);
});
}
void Shader::set_uniform(const std::string &name, GLfloat value1, GLfloat value2, GLfloat value3, GLfloat value4) {
enqueue_function([name, value1, value2, value3, value4, this] {
with_location(glUniform4f, value1, value2, value3, value4);
});
}
void Shader::set_uniform(const std::string &name, GLuint value1, GLuint value2, GLuint value3, GLuint value4) {
enqueue_function([name, value1, value2, value3, value4, this] {
with_location(glUniform4ui, value1, value2, value3, value4);
});
}
void Shader::set_uniform(const std::string &name, GLint size, GLsizei count, const GLint *values) {
std::size_t number_of_values = std::size_t(count) * std::size_t(size);
std::vector<GLint> values_copy(values, values + number_of_values);
enqueue_function([name, size, count, values_copy, this] {
switch(size) {
case 1: with_location(glUniform1iv, count, values_copy.data()); break;
case 2: with_location(glUniform2iv, count, values_copy.data()); break;
case 3: with_location(glUniform3iv, count, values_copy.data()); break;
case 4: with_location(glUniform4iv, count, values_copy.data()); break;
}
});
}
void Shader::set_uniform(const std::string &name, GLint size, GLsizei count, const GLfloat *values) {
std::size_t number_of_values = std::size_t(count) * std::size_t(size);
std::vector<GLfloat> values_copy(values, values + number_of_values);
enqueue_function([name, size, count, values_copy, this] {
switch(size) {
case 1: with_location(glUniform1fv, count, values_copy.data()); break;
case 2: with_location(glUniform2fv, count, values_copy.data()); break;
case 3: with_location(glUniform3fv, count, values_copy.data()); break;
case 4: with_location(glUniform4fv, count, values_copy.data()); break;
}
});
}
void Shader::set_uniform(const std::string &name, GLint size, GLsizei count, const GLuint *values) {
std::size_t number_of_values = std::size_t(count) * std::size_t(size);
std::vector<GLuint> values_copy(values, values + number_of_values);
enqueue_function([name, size, count, values_copy, this] {
switch(size) {
case 1: with_location(glUniform1uiv, count, values_copy.data()); break;
case 2: with_location(glUniform2uiv, count, values_copy.data()); break;
case 3: with_location(glUniform3uiv, count, values_copy.data()); break;
case 4: with_location(glUniform4uiv, count, values_copy.data()); break;
}
});
}
void Shader::set_uniform_matrix(const std::string &name, GLint size, bool transpose, const GLfloat *values) {
set_uniform_matrix(name, size, 1, transpose, values);
}
void Shader::set_uniform_matrix(const std::string &name, GLint size, GLsizei count, bool transpose, const GLfloat *values) {
std::size_t number_of_values = std::size_t(count) * std::size_t(size) * std::size_t(size);
std::vector<GLfloat> values_copy(values, values + number_of_values);
enqueue_function([name, size, count, transpose, values_copy, this] {
GLboolean glTranspose = transpose ? GL_TRUE : GL_FALSE;
switch(size) {
case 2: with_location(glUniformMatrix2fv, count, glTranspose, values_copy.data()); break;
case 3: with_location(glUniformMatrix3fv, count, glTranspose, values_copy.data()); break;
case 4: with_location(glUniformMatrix4fv, count, glTranspose, values_copy.data()); break;
}
});
}
void Shader::enqueue_function(std::function<void(void)> function) {
std::lock_guard<std::mutex> function_guard(function_mutex_);
enqueued_functions_.push_back(function);
}
void Shader::flush_functions() const {
std::lock_guard<std::mutex> function_guard(function_mutex_);
for(std::function<void(void)> function : enqueued_functions_) {
function();
}
enqueued_functions_.clear();
}

View File

@ -16,6 +16,8 @@
#include <string>
#include <vector>
namespace Outputs {
namespace Display {
namespace OpenGL {
/*!
@ -32,6 +34,7 @@ public:
};
struct AttributeBinding {
AttributeBinding(const std::string &name, GLuint index) : name(name), index(index) {}
const std::string name;
const GLuint index;
};
@ -43,6 +46,13 @@ public:
@param attribute_bindings A vector of attribute bindings.
*/
Shader(const std::string &vertex_shader, const std::string &fragment_shader, const std::vector<AttributeBinding> &attribute_bindings = {});
/*!
Attempts to compile a shader, throwing @c VertexShaderCompilationError, @c FragmentShaderCompilationError or @c ProgramLinkageError upon failure.
@param vertex_shader The vertex shader source code.
@param fragment_shader The fragment shader source code.
@param binding_names A list of attributes to generate bindings for; these will be given indices 0, 1, 2 ... n-1.
*/
Shader(const std::string &vertex_shader, const std::string &fragment_shader, const std::vector<std::string> &binding_names);
~Shader();
/*!
@ -52,7 +62,7 @@ public:
Subsequently performs all work queued up for the next bind irrespective of whether a @c glUseProgram call occurred.
*/
void bind();
void bind() const;
/*!
Unbinds the current instance of Shader, if one is bound.
@ -64,14 +74,14 @@ public:
@param name The name of the attribute to locate.
@returns The location of the requested attribute.
*/
GLint get_attrib_location(const std::string &name);
GLint get_attrib_location(const std::string &name) const;
/*!
Performs a @c glGetUniformLocation call.
@param name The name of the uniform to locate.
@returns The location of the requested uniform.
*/
GLint get_uniform_location(const std::string &name);
GLint get_uniform_location(const std::string &name) const;
/*!
Shorthand for an appropriate sequence of:
@ -107,17 +117,21 @@ public:
void set_uniform_matrix(const std::string &name, GLint size, GLsizei count, bool transpose, const GLfloat *values);
private:
void init(const std::string &vertex_shader, const std::string &fragment_shader, const std::vector<AttributeBinding> &attribute_bindings);
GLuint compile_shader(const std::string &source, GLenum type);
GLuint shader_program_;
void flush_functions();
std::vector<std::function<void(void)>> enqueued_functions_;
std::mutex function_mutex_;
void flush_functions() const;
mutable std::vector<std::function<void(void)>> enqueued_functions_;
mutable std::mutex function_mutex_;
protected:
void enqueue_function(std::function<void(void)> function);
};
}
}
}
#endif /* Shader_hpp */

View File

@ -0,0 +1,183 @@
//
// TextureTarget.cpp
// Clock Signal
//
// Created by Thomas Harte on 07/02/2016.
// Copyright 2016 Thomas Harte. All rights reserved.
//
#include "TextureTarget.hpp"
#include <cstdlib>
#include <vector>
#include <stdexcept>
using namespace Outputs::Display::OpenGL;
TextureTarget::TextureTarget(GLsizei width, GLsizei height, GLenum texture_unit, GLint mag_filter, bool has_stencil_buffer) :
width_(width),
height_(height),
texture_unit_(texture_unit) {
// Generate and bind a frame buffer.
test_gl(glGenFramebuffers, 1, &framebuffer_);
test_gl(glBindFramebuffer, GL_FRAMEBUFFER, framebuffer_);
// Round the width and height up to the next power of two.
expanded_width_ = 1;
while(expanded_width_ < width) expanded_width_ <<= 1;
expanded_height_ = 1;
while(expanded_height_ < height) expanded_height_ <<= 1;
// Generate a texture and bind it to the nominated texture unit.
test_gl(glGenTextures, 1, &texture_);
bind_texture();
// Set dimensions and set the user-supplied magnification filter.
test_gl(glTexImage2D, GL_TEXTURE_2D, 0, GL_RGBA, GLsizei(expanded_width_), GLsizei(expanded_height_), 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter);
test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
// Set the texture as colour attachment 0 on the frame buffer.
test_gl(glFramebufferTexture2D, GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture_, 0);
// Also add a stencil buffer if requested.
if(has_stencil_buffer) {
test_gl(glGenRenderbuffers, 1, &renderbuffer_);
test_gl(glBindRenderbuffer, GL_RENDERBUFFER, renderbuffer_);
test_gl(glRenderbufferStorage, GL_RENDERBUFFER, GL_STENCIL_INDEX1, expanded_width_, expanded_height_);
test_gl(glFramebufferRenderbuffer, GL_FRAMEBUFFER, GL_STENCIL_ATTACHMENT, GL_RENDERBUFFER, renderbuffer_);
}
// Check for successful construction.
const auto framebuffer_status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if(framebuffer_status != GL_FRAMEBUFFER_COMPLETE) {
switch(framebuffer_status) {
case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
throw std::runtime_error("GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT");
case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER:
throw std::runtime_error("GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER");
case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER:
throw std::runtime_error("GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER");
case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
throw std::runtime_error("GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT");
case GL_FRAMEBUFFER_UNSUPPORTED:
throw std::runtime_error("GL_FRAMEBUFFER_UNSUPPORTED");
default:
throw std::runtime_error("Framebuffer status incomplete: " + std::to_string(framebuffer_status));
case 0:
test_gl_error();
break;
}
}
// Clear the framebuffer.
test_gl(glClear, GL_COLOR_BUFFER_BIT);
}
TextureTarget::~TextureTarget() {
glDeleteFramebuffers(1, &framebuffer_);
glDeleteTextures(1, &texture_);
if(renderbuffer_) glDeleteRenderbuffers(1, &renderbuffer_);
if(drawing_vertex_array_) glDeleteVertexArrays(1, &drawing_vertex_array_);
if(drawing_array_buffer_) glDeleteBuffers(1, &drawing_array_buffer_);
}
void TextureTarget::bind_framebuffer() {
test_gl(glBindFramebuffer, GL_FRAMEBUFFER, framebuffer_);
test_gl(glViewport, 0, 0, width_, height_);
}
void TextureTarget::bind_texture() const {
test_gl(glActiveTexture, texture_unit_);
test_gl(glBindTexture, GL_TEXTURE_2D, texture_);
}
void TextureTarget::draw(float aspect_ratio, float colour_threshold) const {
if(!pixel_shader_) {
const char *vertex_shader =
"#version 150\n"
"in vec2 texCoord;"
"in vec2 position;"
"out vec2 texCoordVarying;"
"void main(void)"
"{"
"texCoordVarying = texCoord;"
"gl_Position = vec4(position, 0.0, 1.0);"
"}";
const char *fragment_shader =
"#version 150\n"
"in vec2 texCoordVarying;"
"uniform sampler2D texID;"
"uniform float threshold;"
"out vec4 fragColour;"
"void main(void)"
"{"
"fragColour = clamp(texture(texID, texCoordVarying), threshold, 1.0);"
"}";
pixel_shader_.reset(new Shader(vertex_shader, fragment_shader));
pixel_shader_->bind();
test_gl(glGenVertexArrays, 1, &drawing_vertex_array_);
test_gl(glGenBuffers, 1, &drawing_array_buffer_);
test_gl(glBindVertexArray, drawing_vertex_array_);
test_gl(glBindBuffer, GL_ARRAY_BUFFER, drawing_array_buffer_);
const GLint position_attribute = pixel_shader_->get_attrib_location("position");
const GLint tex_coord_attribute = pixel_shader_->get_attrib_location("texCoord");
test_gl(glEnableVertexAttribArray, GLuint(position_attribute));
test_gl(glEnableVertexAttribArray, GLuint(tex_coord_attribute));
const GLsizei vertex_stride = 4 * sizeof(GLfloat);
test_gl(glVertexAttribPointer, GLuint(position_attribute), 2, GL_FLOAT, GL_FALSE, vertex_stride, (void *)0);
test_gl(glVertexAttribPointer, GLuint(tex_coord_attribute), 2, GL_FLOAT, GL_FALSE, vertex_stride, (void *)(2 * sizeof(GLfloat)));
const GLint texIDUniform = pixel_shader_->get_uniform_location("texID");
test_gl(glUniform1i, texIDUniform, GLint(texture_unit_ - GL_TEXTURE0));
threshold_uniform_ = pixel_shader_->get_uniform_location("threshold");
}
if(set_aspect_ratio_ != aspect_ratio) {
set_aspect_ratio_ = aspect_ratio;
float buffer[4*4];
// establish texture coordinates
buffer[2] = 0.0f;
buffer[3] = 0.0f;
buffer[6] = 0.0f;
buffer[7] = float(height_) / float(expanded_height_);
buffer[10] = float(width_) / float(expanded_width_);
buffer[11] = 0.0f;
buffer[14] = buffer[10];
buffer[15] = buffer[7];
// determine positions; rule is to keep the same height and centre
float internal_aspect_ratio = float(width_) / float(height_);
float aspect_ratio_ratio = internal_aspect_ratio / aspect_ratio;
buffer[0] = -aspect_ratio_ratio; buffer[1] = -1.0f;
buffer[4] = -aspect_ratio_ratio; buffer[5] = 1.0f;
buffer[8] = aspect_ratio_ratio; buffer[9] = -1.0f;
buffer[12] = aspect_ratio_ratio; buffer[13] = 1.0f;
// upload buffer
test_gl(glBindBuffer, GL_ARRAY_BUFFER, drawing_array_buffer_);
test_gl(glBufferData, GL_ARRAY_BUFFER, sizeof(buffer), buffer, GL_STATIC_DRAW);
}
pixel_shader_->bind();
test_gl(glUniform1f, threshold_uniform_, colour_threshold);
test_gl(glBindVertexArray, drawing_vertex_array_);
test_gl(glDrawArrays, GL_TRIANGLE_STRIP, 0, 4);
}

View File

@ -9,10 +9,12 @@
#ifndef TextureTarget_hpp
#define TextureTarget_hpp
#include "OpenGL.hpp"
#include "Shaders/Shader.hpp"
#include "../OpenGL.hpp"
#include "Shader.hpp"
#include <memory>
namespace Outputs {
namespace Display {
namespace OpenGL {
/*!
@ -22,15 +24,18 @@ namespace OpenGL {
class TextureTarget {
public:
/*!
Creates a new texture target.
Creates a new texture target. Contents are initially undefined.
Throws ErrorFramebufferIncomplete if creation fails. Leaves both the generated texture and framebuffer bound.
Leaves both the generated texture and framebuffer bound.
@throws std::runtime_error if creation fails.
@param width The width of target to create.
@param height The height of target to create.
@param texture_unit A texture unit on which to bind the texture.
@param has_stencil_buffer A 1-bit stencil buffer is attached if this is @c true; no stencil buffer is attached otherwise.
*/
TextureTarget(GLsizei width, GLsizei height, GLenum texture_unit, GLint mag_filter);
TextureTarget(GLsizei width, GLsizei height, GLenum texture_unit, GLint mag_filter, bool has_stencil_buffer);
~TextureTarget();
/*!
@ -41,19 +46,19 @@ class TextureTarget {
/*!
Binds this target as a texture.
*/
void bind_texture();
void bind_texture() const;
/*!
@returns the width of the texture target.
*/
GLsizei get_width() {
GLsizei get_width() const {
return width_;
}
/*!
@returns the height of the texture target.
*/
GLsizei get_height() {
GLsizei get_height() const {
return height_;
}
@ -68,25 +73,23 @@ class TextureTarget {
0.5f being substituted elsewhere. This provides a way to ensure that the sort of
persistent low-value errors that can result from an IIR are hidden.
*/
void draw(float aspect_ratio, float colour_threshold = 0.0f);
enum {
ErrorFramebufferIncomplete
};
void draw(float aspect_ratio, float colour_threshold = 0.0f) const;
private:
GLuint framebuffer_ = 0, texture_ = 0;
GLsizei width_ = 0, height_ = 0;
GLuint framebuffer_ = 0, texture_ = 0, renderbuffer_ = 0;
const GLsizei width_ = 0, height_ = 0;
GLsizei expanded_width_ = 0, expanded_height_ = 0;
GLenum texture_unit_ = 0;
const GLenum texture_unit_ = 0;
std::unique_ptr<Shader> pixel_shader_;
GLuint drawing_vertex_array_ = 0, drawing_array_buffer_ = 0;
float set_aspect_ratio_ = 0.0f;
mutable std::unique_ptr<Shader> pixel_shader_;
mutable GLuint drawing_vertex_array_ = 0, drawing_array_buffer_ = 0;
mutable float set_aspect_ratio_ = 0.0f;
GLint threshold_uniform_;
mutable GLint threshold_uniform_;
};
}
}
}
#endif /* TextureTarget_hpp */

View File

@ -0,0 +1,686 @@
//
// ScanTarget.cpp
// Clock Signal
//
// Created by Thomas Harte on 05/11/2018.
// Copyright © 2018 Thomas Harte. All rights reserved.
//
#include "ScanTarget.hpp"
#include "OpenGL.hpp"
#include "Primitives/Rectangle.hpp"
#include <cassert>
#include <cstring>
using namespace Outputs::Display::OpenGL;
#ifndef NDEBUG
//#define LOG_LINES
//#define LOG_SCANS
#endif
namespace {
/// The texture unit from which to source input data.
constexpr GLenum SourceDataTextureUnit = GL_TEXTURE0;
/// The texture unit which contains raw line-by-line composite, S-Video or RGB data.
constexpr GLenum UnprocessedLineBufferTextureUnit = GL_TEXTURE1;
/// The texture unit that contains a pre-lowpass-filtered but fixed-resolution version of the chroma signal;
/// this is used when processing composite video only, and for chroma information only. Luminance is calculated
/// at the fidelity permitted by the output target, but my efforts to separate, demodulate and filter
/// chrominance during output without either massively sampling or else incurring significant high-frequency
/// noise that sampling reduces into a Moire, have proven to be unsuccessful for the time being.
constexpr GLenum QAMChromaTextureUnit = GL_TEXTURE2;
/// The texture unit that contains the current display.
constexpr GLenum AccumulationTextureUnit = GL_TEXTURE3;
#define TextureAddress(x, y) (((y) << 11) | (x))
#define TextureAddressGetY(v) uint16_t((v) >> 11)
#define TextureAddressGetX(v) uint16_t((v) & 0x7ff)
#define TextureSub(a, b) (((a) - (b)) & 0x3fffff)
const GLint internalFormatForDepth(std::size_t depth) {
switch(depth) {
default: return GL_FALSE;
case 1: return GL_R8UI;
case 2: return GL_RG8UI;
case 3: return GL_RGB8UI;
case 4: return GL_RGBA8UI;
}
}
const GLenum formatForDepth(std::size_t depth) {
switch(depth) {
default: return GL_FALSE;
case 1: return GL_RED_INTEGER;
case 2: return GL_RG_INTEGER;
case 3: return GL_RGB_INTEGER;
case 4: return GL_RGBA_INTEGER;
}
}
}
template <typename T> void ScanTarget::allocate_buffer(const T &array, GLuint &buffer_name, GLuint &vertex_array_name) {
const auto buffer_size = array.size() * sizeof(array[0]);
test_gl(glGenBuffers, 1, &buffer_name);
test_gl(glBindBuffer, GL_ARRAY_BUFFER, buffer_name);
test_gl(glBufferData, GL_ARRAY_BUFFER, GLsizeiptr(buffer_size), NULL, GL_STREAM_DRAW);
test_gl(glGenVertexArrays, 1, &vertex_array_name);
test_gl(glBindVertexArray, vertex_array_name);
test_gl(glBindBuffer, GL_ARRAY_BUFFER, buffer_name);
}
ScanTarget::ScanTarget(GLuint target_framebuffer, float output_gamma) :
target_framebuffer_(target_framebuffer),
output_gamma_(output_gamma),
unprocessed_line_texture_(LineBufferWidth, LineBufferHeight, UnprocessedLineBufferTextureUnit, GL_NEAREST, false),
full_display_rectangle_(-1.0f, -1.0f, 2.0f, 2.0f) {
// Ensure proper initialisation of the two atomic pointer sets.
read_pointers_.store(write_pointers_);
submit_pointers_.store(write_pointers_);
// Allocate space for the scans and lines.
allocate_buffer(scan_buffer_, scan_buffer_name_, scan_vertex_array_);
allocate_buffer(line_buffer_, line_buffer_name_, line_vertex_array_);
// TODO: if this is OpenGL 4.4 or newer, use glBufferStorage rather than glBufferData
// and specify GL_MAP_PERSISTENT_BIT. Then map the buffer now, and let the client
// write straight into it.
test_gl(glGenTextures, 1, &write_area_texture_name_);
test_gl(glBlendFunc, GL_SRC_ALPHA, GL_CONSTANT_COLOR);
test_gl(glBlendColor, 0.4f, 0.4f, 0.4f, 1.0f);
is_drawing_.clear();
}
ScanTarget::~ScanTarget() {
while(is_drawing_.test_and_set());
glDeleteBuffers(1, &scan_buffer_name_);
glDeleteTextures(1, &write_area_texture_name_);
glDeleteVertexArrays(1, &scan_vertex_array_);
}
void ScanTarget::set_target_framebuffer(GLuint target_framebuffer) {
while(is_drawing_.test_and_set());
target_framebuffer_ = target_framebuffer;
is_drawing_.clear();
}
void ScanTarget::set_modals(Modals modals) {
// Don't change the modals while drawing is ongoing; a previous set might be
// in the process of being established.
while(is_drawing_.test_and_set());
modals_ = modals;
modals_are_dirty_ = true;
is_drawing_.clear();
}
Outputs::Display::ScanTarget::Scan *ScanTarget::begin_scan() {
if(allocation_has_failed_) return nullptr;
const auto result = &scan_buffer_[write_pointers_.scan_buffer];
const auto read_pointers = read_pointers_.load();
// Advance the pointer.
const auto next_write_pointer = decltype(write_pointers_.scan_buffer)((write_pointers_.scan_buffer + 1) % scan_buffer_.size());
// Check whether that's too many.
if(next_write_pointer == read_pointers.scan_buffer) {
allocation_has_failed_ = true;
return nullptr;
}
write_pointers_.scan_buffer = next_write_pointer;
++provided_scans_;
// Fill in extra OpenGL-specific details.
result->line = write_pointers_.line;
vended_scan_ = result;
return &result->scan;
}
void ScanTarget::end_scan() {
if(vended_scan_) {
vended_scan_->data_y = TextureAddressGetY(vended_write_area_pointer_);
vended_scan_->line = write_pointers_.line;
vended_scan_->scan.end_points[0].data_offset += TextureAddressGetX(vended_write_area_pointer_);
vended_scan_->scan.end_points[1].data_offset += TextureAddressGetX(vended_write_area_pointer_);
#ifdef LOG_SCANS
if(vended_scan_->scan.composite_amplitude) {
std::cout << "S: ";
std::cout << vended_scan_->scan.end_points[0].composite_angle << "/" << vended_scan_->scan.end_points[0].data_offset << "/" << vended_scan_->scan.end_points[0].cycles_since_end_of_horizontal_retrace << " -> ";
std::cout << vended_scan_->scan.end_points[1].composite_angle << "/" << vended_scan_->scan.end_points[1].data_offset << "/" << vended_scan_->scan.end_points[1].cycles_since_end_of_horizontal_retrace << " => ";
std::cout << double(vended_scan_->scan.end_points[1].composite_angle - vended_scan_->scan.end_points[0].composite_angle) / (double(vended_scan_->scan.end_points[1].data_offset - vended_scan_->scan.end_points[0].data_offset) * 64.0f) << "/";
std::cout << double(vended_scan_->scan.end_points[1].composite_angle - vended_scan_->scan.end_points[0].composite_angle) / (double(vended_scan_->scan.end_points[1].cycles_since_end_of_horizontal_retrace - vended_scan_->scan.end_points[0].cycles_since_end_of_horizontal_retrace) * 64.0f);
std::cout << std::endl;
}
#endif
}
vended_scan_ = nullptr;
}
uint8_t *ScanTarget::begin_data(size_t required_length, size_t required_alignment) {
if(allocation_has_failed_) return nullptr;
if(!write_area_texture_.size()) {
allocation_has_failed_ = true;
return nullptr;
}
// Determine where the proposed write area would start and end.
uint16_t output_y = TextureAddressGetY(write_pointers_.write_area);
uint16_t aligned_start_x = TextureAddressGetX(write_pointers_.write_area & 0xffff) + 1;
aligned_start_x += uint16_t((required_alignment - aligned_start_x%required_alignment)%required_alignment);
uint16_t end_x = aligned_start_x + uint16_t(1 + required_length);
if(end_x > WriteAreaWidth) {
output_y = (output_y + 1) % WriteAreaHeight;
aligned_start_x = uint16_t(required_alignment);
end_x = aligned_start_x + uint16_t(1 + required_length);
}
// Check whether that steps over the read pointer.
const auto end_address = TextureAddress(end_x, output_y);
const auto read_pointers = read_pointers_.load();
const auto end_distance = TextureSub(end_address, read_pointers.write_area);
const auto previous_distance = TextureSub(write_pointers_.write_area, read_pointers.write_area);
// If allocating this would somehow make the write pointer back away from the read pointer,
// there must not be enough space left.
if(end_distance < previous_distance) {
allocation_has_failed_ = true;
return nullptr;
}
// Everything checks out, return the pointer.
vended_write_area_pointer_ = write_pointers_.write_area = TextureAddress(aligned_start_x, output_y);
return &write_area_texture_[size_t(write_pointers_.write_area) * data_type_size_];
// Note state at exit:
// write_pointers_.write_area points to the first pixel the client is expected to draw to.
}
void ScanTarget::end_data(size_t actual_length) {
if(allocation_has_failed_) return;
// Bookend the start of the new data, to safeguard for precision errors in sampling.
memcpy(
&write_area_texture_[size_t(write_pointers_.write_area - 1) * data_type_size_],
&write_area_texture_[size_t(write_pointers_.write_area) * data_type_size_],
data_type_size_);
// The write area was allocated in the knowledge that there's sufficient
// distance left on the current line, so there's no need to worry about carry.
write_pointers_.write_area += actual_length + 1;
// Also bookend the end.
memcpy(
&write_area_texture_[size_t(write_pointers_.write_area - 1) * data_type_size_],
&write_area_texture_[size_t(write_pointers_.write_area - 2) * data_type_size_],
data_type_size_);
}
void ScanTarget::will_change_owner() {
allocation_has_failed_ = true;
vended_scan_ = nullptr;
}
void ScanTarget::submit() {
if(allocation_has_failed_) {
// Reset all pointers to where they were; this also means
// the stencil won't be properly populated.
write_pointers_ = submit_pointers_.load();
frame_is_complete_ = false;
} else {
// Advance submit pointer.
submit_pointers_.store(write_pointers_);
}
// Continue defaulting to a failed allocation for as long as there isn't a line available.
allocation_has_failed_ = line_allocation_has_failed_;
}
void ScanTarget::announce(Event event, bool is_visible, const Outputs::Display::ScanTarget::Scan::EndPoint &location, uint8_t composite_amplitude) {
if(event == ScanTarget::Event::EndVerticalRetrace) {
// The previous-frame-is-complete flag is subject to a two-slot queue because
// measurement for *this* frame needs to begin now, meaning that the previous
// result needs to be put somewhere. Setting frame_is_complete_ back to true
// only after it has been put somewhere also doesn't work, since if the first
// few lines of a frame are skipped for any reason, there'll be nowhere to
// put it.
is_first_in_frame_ = true;
previous_frame_was_complete_ = frame_is_complete_;
frame_is_complete_ = true;
}
if(output_is_visible_ == is_visible) return;
if(is_visible) {
const auto read_pointers = read_pointers_.load();
// Commit the most recent line only if any scans fell on it.
// Otherwise there's no point outputting it, it'll contribute nothing.
if(provided_scans_) {
// Store metadata if concluding a previous line.
if(active_line_) {
line_metadata_buffer_[size_t(write_pointers_.line)].is_first_in_frame = is_first_in_frame_;
line_metadata_buffer_[size_t(write_pointers_.line)].previous_frame_was_complete = previous_frame_was_complete_;
is_first_in_frame_ = false;
}
// Attempt to allocate a new line; note allocation failure if necessary.
const auto next_line = uint16_t((write_pointers_.line + 1) % LineBufferHeight);
if(next_line == read_pointers.line) {
line_allocation_has_failed_ = allocation_has_failed_ = true;
active_line_ = nullptr;
} else {
line_allocation_has_failed_ = false;
write_pointers_.line = next_line;
active_line_ = &line_buffer_[size_t(write_pointers_.line)];
}
provided_scans_ = 0;
} else {
// Just check whether a new line is available now, if waiting.
if(line_allocation_has_failed_) {
const auto next_line = uint16_t((write_pointers_.line + 1) % LineBufferHeight);
if(next_line != read_pointers.line) {
line_allocation_has_failed_ = false;
write_pointers_.line = next_line;
active_line_ = &line_buffer_[size_t(write_pointers_.line)];
}
}
}
if(active_line_) {
active_line_->end_points[0].x = location.x;
active_line_->end_points[0].y = location.y;
active_line_->end_points[0].cycles_since_end_of_horizontal_retrace = location.cycles_since_end_of_horizontal_retrace;
active_line_->end_points[0].composite_angle = location.composite_angle;
active_line_->line = write_pointers_.line;
active_line_->composite_amplitude = composite_amplitude;
}
} else {
if(active_line_) {
active_line_->end_points[1].x = location.x;
active_line_->end_points[1].y = location.y;
active_line_->end_points[1].cycles_since_end_of_horizontal_retrace = location.cycles_since_end_of_horizontal_retrace;
active_line_->end_points[1].composite_angle = location.composite_angle;
#ifdef LOG_LINES
if(active_line_->composite_amplitude) {
std::cout << "L: ";
std::cout << active_line_->end_points[0].composite_angle << "/" << active_line_->end_points[0].cycles_since_end_of_horizontal_retrace << " -> ";
std::cout << active_line_->end_points[1].composite_angle << "/" << active_line_->end_points[1].cycles_since_end_of_horizontal_retrace << " => ";
std::cout << (active_line_->end_points[1].composite_angle - active_line_->end_points[0].composite_angle) << "/" << (active_line_->end_points[1].cycles_since_end_of_horizontal_retrace - active_line_->end_points[0].cycles_since_end_of_horizontal_retrace) << " => ";
std::cout << double(active_line_->end_points[1].composite_angle - active_line_->end_points[0].composite_angle) / (double(active_line_->end_points[1].cycles_since_end_of_horizontal_retrace - active_line_->end_points[0].cycles_since_end_of_horizontal_retrace) * 64.0f);
std::cout << std::endl;
}
#endif
}
}
output_is_visible_ = is_visible;
}
void ScanTarget::setup_pipeline() {
const auto data_type_size = Outputs::Display::size_for_data_type(modals_.input_data_type);
if(data_type_size != data_type_size_) {
// TODO: flush output.
data_type_size_ = data_type_size;
write_area_texture_.resize(2048*2048*data_type_size_);
write_pointers_.scan_buffer = 0;
write_pointers_.write_area = 0;
}
// Prepare to bind line shaders.
test_gl(glBindVertexArray, line_vertex_array_);
test_gl(glBindBuffer, GL_ARRAY_BUFFER, line_buffer_name_);
// Destroy or create a QAM buffer and shader, if appropriate.
const bool needs_qam_buffer = (modals_.display_type == DisplayType::CompositeColour || modals_.display_type == DisplayType::SVideo);
if(needs_qam_buffer) {
if(!qam_chroma_texture_) {
qam_chroma_texture_.reset(new TextureTarget(LineBufferWidth, LineBufferHeight, QAMChromaTextureUnit, GL_NEAREST, false));
}
qam_separation_shader_ = qam_separation_shader();
enable_vertex_attributes(ShaderType::QAMSeparation, *qam_separation_shader_);
set_uniforms(ShaderType::QAMSeparation, *qam_separation_shader_);
qam_separation_shader_->set_uniform("textureName", GLint(UnprocessedLineBufferTextureUnit - GL_TEXTURE0));
} else {
qam_chroma_texture_.reset();
qam_separation_shader_.reset();
}
// Establish an output shader.
output_shader_ = conversion_shader();
enable_vertex_attributes(ShaderType::Conversion, *output_shader_);
set_uniforms(ShaderType::Conversion, *output_shader_);
output_shader_->set_uniform("origin", modals_.visible_area.origin.x, modals_.visible_area.origin.y);
output_shader_->set_uniform("size", modals_.visible_area.size.width, modals_.visible_area.size.height);
output_shader_->set_uniform("textureName", GLint(UnprocessedLineBufferTextureUnit - GL_TEXTURE0));
output_shader_->set_uniform("qamTextureName", GLint(QAMChromaTextureUnit - GL_TEXTURE0));
// Establish an input shader.
input_shader_ = composition_shader();
test_gl(glBindVertexArray, scan_vertex_array_);
test_gl(glBindBuffer, GL_ARRAY_BUFFER, scan_buffer_name_);
enable_vertex_attributes(ShaderType::Composition, *input_shader_);
set_uniforms(ShaderType::Composition, *input_shader_);
input_shader_->set_uniform("textureName", GLint(SourceDataTextureUnit - GL_TEXTURE0));
}
void ScanTarget::draw(bool synchronous, int output_width, int output_height) {
if(fence_ != nullptr) {
// if the GPU is still busy, don't wait; we'll catch it next time
if(glClientWaitSync(fence_, GL_SYNC_FLUSH_COMMANDS_BIT, synchronous ? GL_TIMEOUT_IGNORED : 0) == GL_TIMEOUT_EXPIRED) {
return;
}
fence_ = nullptr;
}
// Spin until the is-drawing flag is reset; the wait sync above will deal
// with instances where waiting is inappropriate.
while(is_drawing_.test_and_set());
// Establish the pipeline if necessary.
const bool did_setup_pipeline = modals_are_dirty_;
if(modals_are_dirty_) {
setup_pipeline();
modals_are_dirty_ = false;
}
// Grab the current read and submit pointers.
const auto submit_pointers = submit_pointers_.load();
const auto read_pointers = read_pointers_.load();
// Submit scans; only the new ones need to be communicated.
size_t new_scans = (submit_pointers.scan_buffer + scan_buffer_.size() - read_pointers.scan_buffer) % scan_buffer_.size();
if(new_scans) {
test_gl(glBindBuffer, GL_ARRAY_BUFFER, scan_buffer_name_);
// Map only the required portion of the buffer.
const size_t new_scans_size = new_scans * sizeof(Scan);
uint8_t *const destination = static_cast<uint8_t *>(
glMapBufferRange(GL_ARRAY_BUFFER, 0, GLsizeiptr(new_scans_size), GL_MAP_WRITE_BIT | GL_MAP_FLUSH_EXPLICIT_BIT)
);
test_gl_error();
if(read_pointers.scan_buffer < submit_pointers.scan_buffer) {
memcpy(destination, &scan_buffer_[read_pointers.scan_buffer], new_scans_size);
} else {
const size_t first_portion_length = (scan_buffer_.size() - read_pointers.scan_buffer) * sizeof(Scan);
memcpy(destination, &scan_buffer_[read_pointers.scan_buffer], first_portion_length);
memcpy(&destination[first_portion_length], &scan_buffer_[0], new_scans_size - first_portion_length);
}
// Flush and unmap the buffer.
test_gl(glFlushMappedBufferRange, GL_ARRAY_BUFFER, 0, GLsizeiptr(new_scans_size));
test_gl(glUnmapBuffer, GL_ARRAY_BUFFER);
}
// Submit texture.
if(submit_pointers.write_area != read_pointers.write_area) {
test_gl(glActiveTexture, SourceDataTextureUnit);
test_gl(glBindTexture, GL_TEXTURE_2D, write_area_texture_name_);
// Create storage for the texture if it doesn't yet exist; this was deferred until here
// because the pixel format wasn't initially known.
if(!texture_exists_) {
test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
test_gl(glTexImage2D,
GL_TEXTURE_2D,
0,
internalFormatForDepth(data_type_size_),
WriteAreaWidth,
WriteAreaHeight,
0,
formatForDepth(data_type_size_),
GL_UNSIGNED_BYTE,
nullptr);
texture_exists_ = true;
}
const auto start_y = TextureAddressGetY(read_pointers.write_area);
const auto end_y = TextureAddressGetY(submit_pointers.write_area);
if(end_y >= start_y) {
// Submit the direct region from the submit pointer to the read pointer.
test_gl(glTexSubImage2D,
GL_TEXTURE_2D, 0,
0, start_y,
WriteAreaWidth,
1 + end_y - start_y,
formatForDepth(data_type_size_),
GL_UNSIGNED_BYTE,
&write_area_texture_[size_t(TextureAddress(0, start_y)) * data_type_size_]);
} else {
// The circular buffer wrapped around; submit the data from the read pointer to the end of
// the buffer and from the start of the buffer to the submit pointer.
test_gl(glTexSubImage2D,
GL_TEXTURE_2D, 0,
0, 0,
WriteAreaWidth,
1 + end_y,
formatForDepth(data_type_size_),
GL_UNSIGNED_BYTE,
&write_area_texture_[0]);
test_gl(glTexSubImage2D,
GL_TEXTURE_2D, 0,
0, start_y,
WriteAreaWidth,
WriteAreaHeight - start_y,
formatForDepth(data_type_size_),
GL_UNSIGNED_BYTE,
&write_area_texture_[size_t(TextureAddress(0, start_y)) * data_type_size_]);
}
}
// Push new input to the unprocessed line buffer.
if(new_scans) {
unprocessed_line_texture_.bind_framebuffer();
// Clear newly-touched lines; that is everything from (read+1) to submit.
const uint16_t first_line_to_clear = (read_pointers.line+1)%line_buffer_.size();
const uint16_t final_line_to_clear = submit_pointers.line;
if(first_line_to_clear != final_line_to_clear) {
test_gl(glEnable, GL_SCISSOR_TEST);
// Determine the proper clear colour — this needs to be anything that describes black
// in the input colour encoding at use.
if(modals_.input_data_type == InputDataType::Luminance8Phase8) {
// Supply both a zero luminance and a colour-subcarrier-disengaging phase.
test_gl(glClearColor, 0.0f, 1.0f, 0.0f, 0.0f);
} else {
test_gl(glClearColor, 0.0f, 0.0f, 0.0f, 0.0f);
}
if(first_line_to_clear < final_line_to_clear) {
test_gl(glScissor, 0, first_line_to_clear, unprocessed_line_texture_.get_width(), final_line_to_clear - first_line_to_clear);
test_gl(glClear, GL_COLOR_BUFFER_BIT);
} else {
test_gl(glScissor, 0, 0, unprocessed_line_texture_.get_width(), final_line_to_clear);
test_gl(glClear, GL_COLOR_BUFFER_BIT);
test_gl(glScissor, 0, first_line_to_clear, unprocessed_line_texture_.get_width(), unprocessed_line_texture_.get_height() - first_line_to_clear);
test_gl(glClear, GL_COLOR_BUFFER_BIT);
}
test_gl(glDisable, GL_SCISSOR_TEST);
}
// Apply new spans. They definitely always go to the first buffer.
test_gl(glBindVertexArray, scan_vertex_array_);
input_shader_->bind();
test_gl(glDrawArraysInstanced, GL_TRIANGLE_STRIP, 0, 4, GLsizei(new_scans));
}
// Ensure the accumulation buffer is properly sized.
// TODO: based on a decision about host speed, potentially switch to the std::min fragment as shown below,
// which would limit total output buffer size to 1440x1080.
const int framebuffer_height = output_height;//std::min(output_height, 1080);
const int proportional_width = (framebuffer_height * 4) / 3;
const bool did_create_accumulation_texture = !accumulation_texture_ || ( /* !synchronous && */ (accumulation_texture_->get_width() != proportional_width || accumulation_texture_->get_height() != framebuffer_height));
if(did_create_accumulation_texture) {
std::unique_ptr<OpenGL::TextureTarget> new_framebuffer(
new TextureTarget(
GLsizei(proportional_width),
GLsizei(framebuffer_height),
AccumulationTextureUnit,
GL_NEAREST,
true));
if(accumulation_texture_) {
new_framebuffer->bind_framebuffer();
test_gl(glClear, GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
test_gl(glActiveTexture, AccumulationTextureUnit);
accumulation_texture_->bind_texture();
accumulation_texture_->draw(float(output_width) / float(output_height));
test_gl(glClear, GL_STENCIL_BUFFER_BIT);
new_framebuffer->bind_texture();
}
accumulation_texture_ = std::move(new_framebuffer);
// In the absence of a way to resize a stencil buffer, just mark
// what's currently present as invalid to avoid an improper clear
// for this frame.
stencil_is_valid_ = false;
}
if(did_setup_pipeline || did_create_accumulation_texture) {
set_sampling_window(proportional_width, framebuffer_height, *output_shader_);
}
// Figure out how many new lines are ready.
uint16_t new_lines = (submit_pointers.line + LineBufferHeight - read_pointers.line) % LineBufferHeight;
if(new_lines) {
// Prepare to output lines.
test_gl(glBindVertexArray, line_vertex_array_);
// Bind the accumulation framebuffer, unless there's going to be QAM work first.
if(!qam_separation_shader_ || line_metadata_buffer_[read_pointers.line].is_first_in_frame) {
accumulation_texture_->bind_framebuffer();
output_shader_->bind();
// Enable blending and stenciling.
test_gl(glEnable, GL_BLEND);
test_gl(glEnable, GL_STENCIL_TEST);
}
// Set the proper stencil function regardless.
test_gl(glStencilFunc, GL_EQUAL, 0, GLuint(~0));
test_gl(glStencilOp, GL_KEEP, GL_KEEP, GL_INCR);
// Prepare to upload data that will consitute lines.
test_gl(glBindBuffer, GL_ARRAY_BUFFER, line_buffer_name_);
// Divide spans by which frame they're in.
uint16_t start_line = read_pointers.line;
while(new_lines) {
uint16_t end_line = (start_line + 1) % LineBufferHeight;
// Find the limit of spans to draw in this cycle.
size_t lines = 1;
while(end_line != submit_pointers.line && !line_metadata_buffer_[end_line].is_first_in_frame) {
end_line = (end_line + 1) % LineBufferHeight;
++lines;
}
// If this is start-of-frame, clear any untouched pixels and flush the stencil buffer
if(line_metadata_buffer_[start_line].is_first_in_frame) {
if(stencil_is_valid_ && line_metadata_buffer_[start_line].previous_frame_was_complete) {
full_display_rectangle_.draw(0.0f, 0.0f, 0.0f);
}
stencil_is_valid_ = true;
test_gl(glClear, GL_STENCIL_BUFFER_BIT);
// Rebind the program for span output.
test_gl(glBindVertexArray, line_vertex_array_);
if(!qam_separation_shader_) {
output_shader_->bind();
}
}
// Upload.
const auto buffer_size = lines * sizeof(Line);
if(!end_line || end_line > start_line) {
test_gl(glBufferSubData, GL_ARRAY_BUFFER, 0, GLsizeiptr(buffer_size), &line_buffer_[start_line]);
} else {
uint8_t *destination = static_cast<uint8_t *>(
glMapBufferRange(GL_ARRAY_BUFFER, 0, GLsizeiptr(buffer_size), GL_MAP_WRITE_BIT | GL_MAP_FLUSH_EXPLICIT_BIT)
);
assert(destination);
test_gl_error();
const size_t buffer_length = line_buffer_.size() * sizeof(Line);
const size_t start_position = start_line * sizeof(Line);
memcpy(&destination[0], &line_buffer_[start_line], buffer_length - start_position);
memcpy(&destination[buffer_length - start_position], &line_buffer_[0], end_line * sizeof(Line));
test_gl(glFlushMappedBufferRange, GL_ARRAY_BUFFER, 0, GLsizeiptr(buffer_size));
test_gl(glUnmapBuffer, GL_ARRAY_BUFFER);
}
// Produce colour information, if required.
if(qam_separation_shader_) {
qam_separation_shader_->bind();
qam_chroma_texture_->bind_framebuffer();
test_gl(glClear, GL_COLOR_BUFFER_BIT); // TODO: this is here as a hint that the old framebuffer doesn't need reloading;
// test whether that's a valid optimisation on desktop OpenGL.
test_gl(glDisable, GL_BLEND);
test_gl(glDisable, GL_STENCIL_TEST);
test_gl(glDrawArraysInstanced, GL_TRIANGLE_STRIP, 0, 4, GLsizei(lines));
accumulation_texture_->bind_framebuffer();
output_shader_->bind();
test_gl(glEnable, GL_BLEND);
test_gl(glEnable, GL_STENCIL_TEST);
}
// Render to the output.
test_gl(glDrawArraysInstanced, GL_TRIANGLE_STRIP, 0, 4, GLsizei(lines));
start_line = end_line;
new_lines -= lines;
}
// Disable blending and the stencil test again.
test_gl(glDisable, GL_STENCIL_TEST);
test_gl(glDisable, GL_BLEND);
}
// Copy the accumulatiion texture to the target.
test_gl(glBindFramebuffer, GL_FRAMEBUFFER, target_framebuffer_);
test_gl(glViewport, 0, 0, (GLsizei)output_width, (GLsizei)output_height);
test_gl(glClearColor, 0.0f, 0.0f, 0.0f, 0.0f);
test_gl(glClear, GL_COLOR_BUFFER_BIT);
accumulation_texture_->bind_texture();
accumulation_texture_->draw(float(output_width) / float(output_height), 4.0f / 255.0f);
// All data now having been spooled to the GPU, update the read pointers to
// the submit pointer location.
read_pointers_.store(submit_pointers);
// Grab a fence sync object to avoid busy waiting upon the next extry into this
// function, and reset the is_drawing_ flag.
fence_ = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
is_drawing_.clear();
}

View File

@ -0,0 +1,231 @@
//
// ScanTarget.hpp
// Clock Signal
//
// Created by Thomas Harte on 05/11/2018.
// Copyright © 2018 Thomas Harte. All rights reserved.
//
#ifndef ScanTarget_hpp
#define ScanTarget_hpp
#include "../ScanTarget.hpp"
#include "../Log.hpp"
#include "OpenGL.hpp"
#include "Primitives/TextureTarget.hpp"
#include "Primitives/Rectangle.hpp"
#include "../../SignalProcessing/FIRFilter.hpp"
#include <array>
#include <atomic>
#include <cstdint>
#include <list>
#include <memory>
#include <string>
#include <vector>
namespace Outputs {
namespace Display {
namespace OpenGL {
/*!
Provides a ScanTarget that uses OpenGL to render its output;
this uses various internal buffers so that the only geometry
drawn to the target framebuffer is a quad.
*/
class ScanTarget: public Outputs::Display::ScanTarget {
public:
ScanTarget(GLuint target_framebuffer = 0, float output_gamma = 2.2f);
~ScanTarget();
void set_target_framebuffer(GLuint);
void draw(bool synchronous, int output_width, int output_height);
private:
#ifndef NDEBUG
struct OpenGLVersionDumper {
OpenGLVersionDumper() {
// Note the OpenGL version, as the first thing this class does prior to construction.
LOG("Constructing scan target with OpenGL " << glGetString(GL_VERSION) << "; shading language version " << glGetString(GL_SHADING_LANGUAGE_VERSION));
}
} dumper_;
#endif
static constexpr int WriteAreaWidth = 2048;
static constexpr int WriteAreaHeight = 2048;
static constexpr int LineBufferWidth = 2048;
static constexpr int LineBufferHeight = 2048;
GLuint target_framebuffer_;
const float output_gamma_;
// Outputs::Display::ScanTarget overrides.
void set_modals(Modals) override;
Scan *begin_scan() override;
void end_scan() override;
uint8_t *begin_data(size_t required_length, size_t required_alignment) override;
void end_data(size_t actual_length) override;
void submit() override;
void announce(Event event, bool is_visible, const Outputs::Display::ScanTarget::Scan::EndPoint &location, uint8_t colour_burst_amplitude) override;
void will_change_owner() override;
bool output_is_visible_ = false;
// Extends the definition of a Scan to include two extra fields,
// relevant to the way that this scan target processes video.
struct Scan {
Outputs::Display::ScanTarget::Scan scan;
/// Stores the y coordinate that this scan's data is at, within the write area texture.
uint16_t data_y;
/// Stores the y coordinate of this scan within the line buffer.
uint16_t line;
};
struct PointerSet {
// This constructor is here to appease GCC's interpretation of
// an ambiguity in the C++ standard; cf. https://stackoverflow.com/questions/17430377
PointerSet() noexcept {}
// The sizes below might be less hassle as something more natural like ints,
// but squeezing this struct into 64 bits makes the std::atomics more likely
// to be lock free; they are under LLVM x86-64.
int write_area = 0;
uint16_t scan_buffer = 0;
uint16_t line = 0;
};
/// A pointer to the next thing that should be provided to the caller for data.
PointerSet write_pointers_;
/// A pointer to the final thing currently cleared for submission.
std::atomic<PointerSet> submit_pointers_;
/// A pointer to the first thing not yet submitted for display.
std::atomic<PointerSet> read_pointers_;
// Maintains a buffer of the most recent 3072 scans.
std::array<Scan, 3072> scan_buffer_;
// Maintains a list of composite scan buffer coordinates; the Line struct
// is transported to the GPU in its entirety; the LineMetadatas live in CPU
// space only.
struct Line {
struct EndPoint {
uint16_t x, y;
uint16_t cycles_since_end_of_horizontal_retrace;
int16_t composite_angle;
} end_points[2];
uint16_t line;
uint8_t composite_amplitude;
};
struct LineMetadata {
bool is_first_in_frame;
bool previous_frame_was_complete;
};
std::array<Line, LineBufferHeight> line_buffer_;
std::array<LineMetadata, LineBufferHeight> line_metadata_buffer_;
// Contains the first composition of scans into lines;
// they're accumulated prior to output to allow for continuous
// application of any necessary conversions — e.g. composite processing.
TextureTarget unprocessed_line_texture_;
// Contains pre-lowpass-filtered chrominance information that is
// part-QAM-demoduled, if dealing with a QAM data source.
std::unique_ptr<TextureTarget> qam_chroma_texture_;
// Scans are accumulated to the accumulation texture; the full-display
// rectangle is used to ensure untouched pixels properly decay.
std::unique_ptr<TextureTarget> accumulation_texture_;
Rectangle full_display_rectangle_;
bool stencil_is_valid_ = false;
// Ephemeral state that helps in line composition.
Line *active_line_ = nullptr;
int provided_scans_ = 0;
bool is_first_in_frame_ = true;
bool frame_is_complete_ = true;
bool previous_frame_was_complete_ = true;
// OpenGL storage handles for buffer data.
GLuint scan_buffer_name_ = 0, scan_vertex_array_ = 0;
GLuint line_buffer_name_ = 0, line_vertex_array_ = 0;
template <typename T> void allocate_buffer(const T &array, GLuint &buffer_name, GLuint &vertex_array_name);
template <typename T> void patch_buffer(const T &array, GLuint target, uint16_t submit_pointer, uint16_t read_pointer);
// Uses a texture to vend write areas.
std::vector<uint8_t> write_area_texture_;
size_t data_type_size_ = 0;
GLuint write_area_texture_name_ = 0;
bool texture_exists_ = false;
// Ephemeral information for the begin/end functions.
Scan *vended_scan_ = nullptr;
int vended_write_area_pointer_ = 0;
// Track allocation failures.
bool allocation_has_failed_ = false;
bool line_allocation_has_failed_ = false;
// Receives scan target modals.
Modals modals_;
bool modals_are_dirty_ = false;
void setup_pipeline();
enum class ShaderType {
Composition,
Conversion,
QAMSeparation
};
/*!
Calls @c taret.enable_vertex_attribute_with_pointer to attach all
globals for shaders of @c type to @c target.
*/
static void enable_vertex_attributes(ShaderType type, Shader &target);
void set_uniforms(ShaderType type, Shader &target) const;
std::vector<std::string> bindings(ShaderType type) const;
GLsync fence_ = nullptr;
std::atomic_flag is_drawing_;
std::unique_ptr<Shader> input_shader_;
std::unique_ptr<Shader> output_shader_;
std::unique_ptr<Shader> qam_separation_shader_;
/*!
Produces a shader that composes fragment of the input stream to a single buffer,
normalising the data into one of four forms: RGB, 8-bit luminance,
phase-linked luminance or luminance+phase offset.
*/
std::unique_ptr<Shader> composition_shader() const;
/*!
Produces a shader that reads from a composition buffer and converts to host
output RGB, decoding composite or S-Video as necessary.
*/
std::unique_ptr<Shader> conversion_shader() const;
/*!
Produces a shader that writes separated but not-yet filtered QAM components
from the unprocessed line texture to the QAM chroma texture, at a fixed
size of four samples per colour clock, point sampled.
*/
std::unique_ptr<Shader> qam_separation_shader() const;
void set_sampling_window(int output_Width, int output_height, Shader &target);
std::string sampling_function() const;
};
}
}
}
#endif /* ScanTarget_hpp */

View File

@ -0,0 +1,664 @@
//
// ScanTargetVertexArrayAttributs.cpp
// Clock Signal
//
// Created by Thomas Harte on 11/11/2018.
// Copyright © 2018 Thomas Harte. All rights reserved.
//
#include "ScanTarget.hpp"
#include <cmath>
using namespace Outputs::Display::OpenGL;
// MARK: - State setup for compiled shaders.
void ScanTarget::set_uniforms(ShaderType type, Shader &target) const {
// Slightly over-amping rowHeight here is a cheap way to make sure that lines
// converge even allowing for the fact that they may not be spaced by exactly
// the expected distance. Cf. the stencil-powered logic for making sure all
// pixels are painted only exactly once per field.
switch(type) {
case ShaderType::Composition: break;
default:
target.set_uniform("rowHeight", GLfloat(1.05f / modals_.expected_vertical_lines));
target.set_uniform("scale", GLfloat(modals_.output_scale.x), GLfloat(modals_.output_scale.y));
target.set_uniform("phaseOffset", GLfloat(modals_.input_data_tweaks.phase_linked_luminance_offset));
const float clocks_per_angle = float(modals_.cycles_per_line) * float(modals_.colour_cycle_denominator) / float(modals_.colour_cycle_numerator);
GLfloat texture_offsets[4];
GLfloat angles[4];
for(int c = 0; c < 4; ++c) {
GLfloat angle = (GLfloat(c) - 1.5f) / 4.0f;
texture_offsets[c] = angle * clocks_per_angle;
angles[c] = GLfloat(angle * 2.0f * M_PI);
}
target.set_uniform("textureCoordinateOffsets", 1, 4, texture_offsets);
target.set_uniform("compositeAngleOffsets", 4, 1, angles);
switch(modals_.composite_colour_space) {
case ColourSpace::YIQ: {
const GLfloat rgbToYIQ[] = {0.299f, 0.596f, 0.211f, 0.587f, -0.274f, -0.523f, 0.114f, -0.322f, 0.312f};
const GLfloat yiqToRGB[] = {1.0f, 1.0f, 1.0f, 0.956f, -0.272f, -1.106f, 0.621f, -0.647f, 1.703f};
target.set_uniform_matrix("lumaChromaToRGB", 3, false, yiqToRGB);
target.set_uniform_matrix("rgbToLumaChroma", 3, false, rgbToYIQ);
} break;
case ColourSpace::YUV: {
const GLfloat rgbToYUV[] = {0.299f, -0.14713f, 0.615f, 0.587f, -0.28886f, -0.51499f, 0.114f, 0.436f, -0.10001f};
const GLfloat yuvToRGB[] = {1.0f, 1.0f, 1.0f, 0.0f, -0.39465f, 2.03211f, 1.13983f, -0.58060f, 0.0f};
target.set_uniform_matrix("lumaChromaToRGB", 3, false, yuvToRGB);
target.set_uniform_matrix("rgbToLumaChroma", 3, false, rgbToYUV);
} break;
}
break;
}
}
void ScanTarget::set_sampling_window(int output_width, int output_height, Shader &target) {
if(modals_.display_type != DisplayType::CompositeColour) {
const float one_pixel_width = float(modals_.cycles_per_line) * modals_.visible_area.size.width / float(output_width);
const float clocks_per_angle = float(modals_.cycles_per_line) * float(modals_.colour_cycle_denominator) / float(modals_.colour_cycle_numerator);
GLfloat texture_offsets[4];
GLfloat angles[4];
for(int c = 0; c < 4; ++c) {
texture_offsets[c] = 1.5f * (((one_pixel_width * float(c)) / 3.0f) - (one_pixel_width * 0.5f));
angles[c] = GLfloat((texture_offsets[c] / clocks_per_angle) * 2.0f * M_PI);
}
target.set_uniform("textureCoordinateOffsets", 1, 4, texture_offsets);
target.set_uniform("compositeAngleOffsets", 4, 1, angles);
}
}
void ScanTarget::enable_vertex_attributes(ShaderType type, Shader &target) {
#define rt_offset_of(field, source) (reinterpret_cast<uint8_t *>(&source.field) - reinterpret_cast<uint8_t *>(&source))
// test_scan and test_line are here so that the byte offsets that need to be
// calculated inside a loop can be done so validly; offsetof requires constant arguments.
Scan test_scan;
Line test_line;
switch(type) {
case ShaderType::Composition:
for(int c = 0; c < 2; ++c) {
const std::string prefix = c ? "end" : "start";
target.enable_vertex_attribute_with_pointer(
prefix + "DataX",
1, GL_UNSIGNED_SHORT, GL_FALSE,
sizeof(Scan),
reinterpret_cast<void *>(rt_offset_of(scan.end_points[c].data_offset, test_scan)),
1);
target.enable_vertex_attribute_with_pointer(
prefix + "Clock",
1, GL_UNSIGNED_SHORT, GL_FALSE,
sizeof(Scan),
reinterpret_cast<void *>(rt_offset_of(scan.end_points[c].cycles_since_end_of_horizontal_retrace, test_scan)),
1);
}
target.enable_vertex_attribute_with_pointer(
"dataY",
1, GL_UNSIGNED_SHORT, GL_FALSE,
sizeof(Scan),
reinterpret_cast<void *>(offsetof(Scan, data_y)),
1);
target.enable_vertex_attribute_with_pointer(
"lineY",
1, GL_UNSIGNED_SHORT, GL_FALSE,
sizeof(Scan),
reinterpret_cast<void *>(offsetof(Scan, line)),
1);
break;
default:
for(int c = 0; c < 2; ++c) {
const std::string prefix = c ? "end" : "start";
if(type == ShaderType::Conversion) {
target.enable_vertex_attribute_with_pointer(
prefix + "Point",
2, GL_UNSIGNED_SHORT, GL_FALSE,
sizeof(Line),
reinterpret_cast<void *>(rt_offset_of(end_points[c].x, test_line)),
1);
}
target.enable_vertex_attribute_with_pointer(
prefix + "Clock",
1, GL_UNSIGNED_SHORT, GL_FALSE,
sizeof(Line),
reinterpret_cast<void *>(rt_offset_of(end_points[c].cycles_since_end_of_horizontal_retrace, test_line)),
1);
target.enable_vertex_attribute_with_pointer(
prefix + "CompositeAngle",
1, GL_SHORT, GL_FALSE,
sizeof(Line),
reinterpret_cast<void *>(rt_offset_of(end_points[c].composite_angle, test_line)),
1);
}
target.enable_vertex_attribute_with_pointer(
"lineY",
1, GL_UNSIGNED_SHORT, GL_FALSE,
sizeof(Line),
reinterpret_cast<void *>(offsetof(Line, line)),
1);
target.enable_vertex_attribute_with_pointer(
"lineCompositeAmplitude",
1, GL_UNSIGNED_BYTE, GL_FALSE,
sizeof(Line),
reinterpret_cast<void *>(offsetof(Line, composite_amplitude)),
1);
break;
}
#undef rt_offset_of
}
std::vector<std::string> ScanTarget::bindings(ShaderType type) const {
switch(type) {
case ShaderType::Composition: return {
"startDataX",
"startClock",
"endDataX",
"endClock",
"dataY",
"lineY"
};
default: return {
"startPoint",
"endPoint",
"startClock",
"endClock",
"lineY",
"lineCompositeAmplitude",
"startCompositeAngle",
"endCompositeAngle"
};
}
}
// MARK: - Shader code.
std::string ScanTarget::sampling_function() const {
std::string fragment_shader;
if(modals_.display_type == DisplayType::SVideo) {
fragment_shader +=
"vec2 svideo_sample(vec2 coordinate, float angle) {";
} else {
fragment_shader +=
"float composite_sample(vec2 coordinate, float angle) {";
}
const bool is_svideo = modals_.display_type == DisplayType::SVideo;
switch(modals_.input_data_type) {
case InputDataType::Luminance1:
case InputDataType::Luminance8:
// Easy, just copy across.
fragment_shader +=
is_svideo ?
"return vec2(textureLod(textureName, coordinate, 0).r, 0.0);" :
"return textureLod(textureName, coordinate, 0).r;";
break;
case InputDataType::PhaseLinkedLuminance8:
fragment_shader +=
"uint iPhase = uint(step(sign(angle), 0.0) * 3) ^ uint(abs(angle * 2.0 / 3.141592654) ) & 3u;";
fragment_shader +=
is_svideo ?
"return vec2(textureLod(textureName, coordinate, 0)[iPhase], 0.0);" :
"return textureLod(textureName, coordinate, 0)[iPhase];";
break;
case InputDataType::Luminance8Phase8:
fragment_shader +=
"vec2 yc = textureLod(textureName, coordinate, 0).rg;"
"float phaseOffset = 3.141592654 * 2.0 * 2.0 * yc.y;"
"float rawChroma = step(yc.y, 0.75) * cos(angle + phaseOffset);";
fragment_shader +=
is_svideo ?
"return vec2(yc.x, rawChroma);" :
"return mix(yc.x, rawChroma, compositeAmplitude);";
break;
case InputDataType::Red1Green1Blue1:
case InputDataType::Red2Green2Blue2:
case InputDataType::Red4Green4Blue4:
case InputDataType::Red8Green8Blue8:
fragment_shader +=
"vec3 colour = rgbToLumaChroma * textureLod(textureName, coordinate, 0).rgb;"
"vec2 quadrature = vec2(cos(angle), sin(angle));";
fragment_shader +=
is_svideo ?
"return vec2(colour.r, dot(quadrature, colour.gb));" :
"return mix(colour.r, dot(quadrature, colour.gb), compositeAmplitude);";
break;
}
fragment_shader += "}";
return fragment_shader;
}
std::unique_ptr<Shader> ScanTarget::conversion_shader() const {
// Compose a vertex shader. If the display type is RGB, generate just the proper
// geometry position, plus a solitary textureCoordinate.
//
// If the display type is anything other than RGB, also produce composite
// angle and 1/composite amplitude as outputs.
//
// If the display type is composite colour, generate four textureCoordinates,
// spanning a range of -135, -45, +45, +135 degrees.
//
// If the display type is S-Video, generate three textureCoordinates, at
// -45, 0, +45.
std::string vertex_shader =
"#version 150\n"
"uniform vec2 scale;"
"uniform float rowHeight;"
"in vec2 startPoint;"
"in vec2 endPoint;"
"in float startClock;"
"in float startCompositeAngle;"
"in float endClock;"
"in float endCompositeAngle;"
"in float lineY;"
"in float lineCompositeAmplitude;"
"uniform sampler2D textureName;"
"uniform sampler2D qamTextureName;"
"uniform vec2 origin;"
"uniform vec2 size;"
"uniform float textureCoordinateOffsets[4];"
"out vec2 textureCoordinates[4];";
std::string fragment_shader =
"#version 150\n"
"uniform sampler2D textureName;"
"uniform sampler2D qamTextureName;"
"in vec2 textureCoordinates[4];"
"out vec4 fragColour;";
if(modals_.display_type != DisplayType::RGB) {
vertex_shader +=
"out float compositeAngle;"
"out float compositeAmplitude;"
"out float oneOverCompositeAmplitude;"
"uniform float angleOffsets[4];";
fragment_shader +=
"in float compositeAngle;"
"in float compositeAmplitude;"
"in float oneOverCompositeAmplitude;"
"uniform vec4 compositeAngleOffsets;";
}
if(modals_.display_type == DisplayType::SVideo || modals_.display_type == DisplayType::CompositeColour) {
vertex_shader += "out vec2 qamTextureCoordinates[4];";
fragment_shader += "in vec2 qamTextureCoordinates[4];";
}
// Add the code to generate a proper output position; this applies to all display types.
vertex_shader +=
"void main(void) {"
"float lateral = float(gl_VertexID & 1);"
"float longitudinal = float((gl_VertexID & 2) >> 1);"
"vec2 centrePoint = mix(startPoint, vec2(endPoint.x, startPoint.y), lateral) / scale;"
"vec2 height = normalize(vec2(endPoint.x, startPoint.y) - startPoint).yx * (longitudinal - 0.5) * rowHeight;"
"vec2 eyePosition = vec2(-1.0, 1.0) + vec2(2.0, -2.0) * (((centrePoint + height) - origin) / size);"
"gl_Position = vec4(eyePosition, 0.0, 1.0);";
// For everything other than RGB, calculate the two composite outputs.
if(modals_.display_type != DisplayType::RGB) {
vertex_shader +=
"compositeAngle = (mix(startCompositeAngle, endCompositeAngle, lateral) / 32.0) * 3.141592654;"
"compositeAmplitude = lineCompositeAmplitude / 255.0;"
"oneOverCompositeAmplitude = mix(0.0, 255.0 / lineCompositeAmplitude, step(0.01, lineCompositeAmplitude));";
}
vertex_shader +=
"float centreClock = mix(startClock, endClock, lateral);"
"textureCoordinates[0] = vec2(centreClock + textureCoordinateOffsets[0], lineY + 0.5) / textureSize(textureName, 0);"
"textureCoordinates[1] = vec2(centreClock + textureCoordinateOffsets[1], lineY + 0.5) / textureSize(textureName, 0);"
"textureCoordinates[2] = vec2(centreClock + textureCoordinateOffsets[2], lineY + 0.5) / textureSize(textureName, 0);"
"textureCoordinates[3] = vec2(centreClock + textureCoordinateOffsets[3], lineY + 0.5) / textureSize(textureName, 0);";
if((modals_.display_type == DisplayType::SVideo) || (modals_.display_type == DisplayType::CompositeColour)) {
vertex_shader +=
"float centreCompositeAngle = abs(mix(startCompositeAngle, endCompositeAngle, lateral)) * 4.0 / 64.0;"
"centreCompositeAngle = floor(centreCompositeAngle);"
"qamTextureCoordinates[0] = vec2(centreCompositeAngle - 1.5, lineY + 0.5) / textureSize(textureName, 0);"
"qamTextureCoordinates[1] = vec2(centreCompositeAngle - 0.5, lineY + 0.5) / textureSize(textureName, 0);"
"qamTextureCoordinates[2] = vec2(centreCompositeAngle + 0.5, lineY + 0.5) / textureSize(textureName, 0);"
"qamTextureCoordinates[3] = vec2(centreCompositeAngle + 1.5, lineY + 0.5) / textureSize(textureName, 0);";
}
vertex_shader += "}";
// Compose a fragment shader.
if(modals_.display_type != DisplayType::RGB) {
fragment_shader +=
"uniform mat3 lumaChromaToRGB;"
"uniform mat3 rgbToLumaChroma;";
fragment_shader += sampling_function();
}
fragment_shader +=
"void main(void) {"
"vec3 fragColour3;";
switch(modals_.display_type) {
case DisplayType::CompositeColour:
fragment_shader +=
"vec4 angles = compositeAngle + compositeAngleOffsets;"
// Sample four times over, at proper angle offsets.
"vec4 samples = vec4("
"composite_sample(textureCoordinates[0], angles.x),"
"composite_sample(textureCoordinates[1], angles.y),"
"composite_sample(textureCoordinates[2], angles.z),"
"composite_sample(textureCoordinates[3], angles.w)"
");"
// Compute a luminance for use if there's no colour information, now, before
// modifying samples.
"float mono_luminance = dot(samples, vec4(0.15, 0.35, 0.35, 0.15));"
// Take the average to calculate luminance, then subtract that from all four samples to
// give chrominance.
"float luminance = dot(samples, vec4(0.25));"
// Split and average chrominance.
"vec2 chrominances[4] = vec2[4]("
"textureLod(qamTextureName, qamTextureCoordinates[0], 0).gb,"
"textureLod(qamTextureName, qamTextureCoordinates[1], 0).gb,"
"textureLod(qamTextureName, qamTextureCoordinates[2], 0).gb,"
"textureLod(qamTextureName, qamTextureCoordinates[3], 0).gb"
");"
"vec2 channels = (chrominances[0] + chrominances[1] + chrominances[2] + chrominances[3])*0.5 - vec2(1.0);"
// Apply a colour space conversion to get RGB.
"fragColour3 = mix("
"lumaChromaToRGB * vec3(luminance / (1.0 - compositeAmplitude), channels),"
"vec3(mono_luminance),"
"step(oneOverCompositeAmplitude, 0.01)"
");";
break;
case DisplayType::CompositeMonochrome:
fragment_shader +=
"vec4 angles = compositeAngle + compositeAngleOffsets;"
"vec4 samples = vec4("
"composite_sample(textureCoordinates[0], angles.x),"
"composite_sample(textureCoordinates[1], angles.y),"
"composite_sample(textureCoordinates[2], angles.z),"
"composite_sample(textureCoordinates[3], angles.w)"
");"
"fragColour3 = vec3(dot(samples, vec4(0.15, 0.35, 0.35, 0.25)));";
break;
case DisplayType::RGB:
fragment_shader +=
"vec3 samples[4] = vec3[4]("
"textureLod(textureName, textureCoordinates[0], 0).rgb,"
"textureLod(textureName, textureCoordinates[1], 0).rgb,"
"textureLod(textureName, textureCoordinates[2], 0).rgb,"
"textureLod(textureName, textureCoordinates[3], 0).rgb"
");"
"fragColour3 = samples[0]*0.15 + samples[1]*0.35 + samples[2]*0.35 + samples[2]*0.15;";
break;
case DisplayType::SVideo:
fragment_shader +=
// Sample the S-Video stream to obtain luminance.
"vec4 angles = compositeAngle + compositeAngleOffsets;"
"vec4 samples = vec4("
"svideo_sample(textureCoordinates[0], angles.x).x,"
"svideo_sample(textureCoordinates[1], angles.y).x,"
"svideo_sample(textureCoordinates[2], angles.z).x,"
"svideo_sample(textureCoordinates[3], angles.w).x"
");"
"float luminance = dot(samples, vec4(0.15, 0.35, 0.35, 0.25));"
// Split and average chrominaxnce.
"vec2 chrominances[4] = vec2[4]("
"textureLod(qamTextureName, qamTextureCoordinates[0], 0).gb,"
"textureLod(qamTextureName, qamTextureCoordinates[1], 0).gb,"
"textureLod(qamTextureName, qamTextureCoordinates[2], 0).gb,"
"textureLod(qamTextureName, qamTextureCoordinates[3], 0).gb"
");"
"vec2 channels = (chrominances[0] + chrominances[1] + chrominances[2] + chrominances[3])*0.5 - vec2(1.0);"
// Apply a colour space conversion to get RGB.
"fragColour3 = lumaChromaToRGB * vec3(luminance, channels);";
break;
}
// Apply a brightness adjustment if requested.
if(fabs(modals_.brightness - 1.0f) > 0.05f) {
fragment_shader += "fragColour3 = fragColour3 * " + std::to_string(modals_.brightness) + ";";
}
// Apply a gamma correction if required.
if(fabs(output_gamma_ - modals_.intended_gamma) > 0.05f) {
const float gamma_ratio = output_gamma_ / modals_.intended_gamma;
fragment_shader += "fragColour3 = pow(fragColour3, vec3(" + std::to_string(gamma_ratio) + "));";
}
fragment_shader +=
"fragColour = vec4(fragColour3, 0.64);"
"}";
return std::unique_ptr<Shader>(new Shader(
vertex_shader,
fragment_shader,
bindings(ShaderType::Conversion)
));
}
std::unique_ptr<Shader> ScanTarget::composition_shader() const {
const std::string vertex_shader =
"#version 150\n"
"in float startDataX;"
"in float startClock;"
"in float endDataX;"
"in float endClock;"
"in float dataY;"
"in float lineY;"
"out vec2 textureCoordinate;"
"uniform usampler2D textureName;"
"void main(void) {"
"float lateral = float(gl_VertexID & 1);"
"float longitudinal = float((gl_VertexID & 2) >> 1);"
"textureCoordinate = vec2(mix(startDataX, endDataX, lateral), dataY + 0.5) / textureSize(textureName, 0);"
"vec2 eyePosition = vec2(mix(startClock, endClock, lateral), lineY + longitudinal) / vec2(2048.0, 2048.0);"
"gl_Position = vec4(eyePosition*2.0 - vec2(1.0), 0.0, 1.0);"
"}";
std::string fragment_shader =
"#version 150\n"
"out vec4 fragColour;"
"in vec2 textureCoordinate;"
"uniform usampler2D textureName;"
"void main(void) {";
switch(modals_.input_data_type) {
case InputDataType::Luminance1:
fragment_shader += "fragColour = textureLod(textureName, textureCoordinate, 0).rrrr;";
break;
case InputDataType::Luminance8:
fragment_shader += "fragColour = textureLod(textureName, textureCoordinate, 0).rrrr / vec4(255.0);";
break;
case InputDataType::PhaseLinkedLuminance8:
case InputDataType::Luminance8Phase8:
case InputDataType::Red8Green8Blue8:
fragment_shader += "fragColour = textureLod(textureName, textureCoordinate, 0) / vec4(255.0);";
break;
case InputDataType::Red1Green1Blue1:
fragment_shader += "fragColour = vec4(textureLod(textureName, textureCoordinate, 0).rrr & uvec3(4u, 2u, 1u), 1.0);";
break;
case InputDataType::Red2Green2Blue2:
fragment_shader +=
"uint textureValue = textureLod(textureName, textureCoordinate, 0).r;"
"fragColour = vec4(float((textureValue >> 4) & 3u), float((textureValue >> 2) & 3u), float(textureValue & 3u), 3.0) / 3.0;";
break;
case InputDataType::Red4Green4Blue4:
fragment_shader +=
"uvec2 textureValue = textureLod(textureName, textureCoordinate, 0).rg;"
"fragColour = vec4(float(textureValue.r) / 15.0, float(textureValue.g & 240u) / 240.0, float(textureValue.g & 15u) / 15.0, 1.0);";
break;
}
return std::unique_ptr<Shader>(new Shader(
vertex_shader,
fragment_shader + "}",
bindings(ShaderType::Composition)
));
}
std::unique_ptr<Shader> ScanTarget::qam_separation_shader() const {
const bool is_svideo = modals_.display_type == DisplayType::SVideo;
// Sets up texture coordinates to run between startClock and endClock, mapping to
// coordinates that correlate with four times the absolute value of the composite angle.
std::string vertex_shader =
"#version 150\n"
"in float startClock;"
"in float startCompositeAngle;"
"in float endClock;"
"in float endCompositeAngle;"
"in float lineY;"
"in float lineCompositeAmplitude;"
"uniform sampler2D textureName;"
"uniform float textureCoordinateOffsets[4];"
"out float compositeAngle;"
"out float compositeAmplitude;"
"out float oneOverCompositeAmplitude;";
std::string fragment_shader =
"#version 150\n"
"uniform sampler2D textureName;"
"uniform mat3 rgbToLumaChroma;"
"in float compositeAngle;"
"in float compositeAmplitude;"
"in float oneOverCompositeAmplitude;"
"out vec4 fragColour;"
"uniform vec4 compositeAngleOffsets;";
if(is_svideo) {
vertex_shader += "out vec2 textureCoordinate;";
fragment_shader += "in vec2 textureCoordinate;";
} else {
vertex_shader += "out vec2 textureCoordinates[4];";
fragment_shader += "in vec2 textureCoordinates[4];";
}
vertex_shader +=
"void main(void) {"
"float lateral = float(gl_VertexID & 1);"
"float longitudinal = float((gl_VertexID & 2) >> 1);"
"float centreClock = mix(startClock, endClock, lateral);"
"compositeAngle = mix(startCompositeAngle, endCompositeAngle, lateral) / 64.0;"
"float snappedCompositeAngle = floor(abs(compositeAngle) * 4.0);"
"vec2 eyePosition = vec2(snappedCompositeAngle, lineY + longitudinal) / vec2(2048.0, 2048.0);"
"gl_Position = vec4(eyePosition*2.0 - vec2(1.0), 0.0, 1.0);"
"compositeAngle = compositeAngle * 2.0 * 3.141592654;"
"compositeAmplitude = lineCompositeAmplitude / 255.0;"
"oneOverCompositeAmplitude = mix(0.0, 255.0 / lineCompositeAmplitude, step(0.01, lineCompositeAmplitude));";
if(is_svideo) {
vertex_shader +=
"textureCoordinate = vec2(centreClock, lineY + 0.5) / textureSize(textureName, 0);";
} else {
vertex_shader +=
"textureCoordinates[0] = vec2(centreClock + textureCoordinateOffsets[0], lineY + 0.5) / textureSize(textureName, 0);"
"textureCoordinates[1] = vec2(centreClock + textureCoordinateOffsets[1], lineY + 0.5) / textureSize(textureName, 0);"
"textureCoordinates[2] = vec2(centreClock + textureCoordinateOffsets[2], lineY + 0.5) / textureSize(textureName, 0);"
"textureCoordinates[3] = vec2(centreClock + textureCoordinateOffsets[3], lineY + 0.5) / textureSize(textureName, 0);";
}
vertex_shader += "}";
fragment_shader +=
sampling_function() +
"void main(void) {";
if(modals_.display_type == DisplayType::SVideo) {
fragment_shader +=
"fragColour = vec4(svideo_sample(textureCoordinate, compositeAngle).rgg * vec3(1.0, cos(compositeAngle), sin(compositeAngle)), 1.0);";
} else {
fragment_shader +=
"vec4 angles = compositeAngle + compositeAngleOffsets;"
// Sample four times over, at proper angle offsets.
"vec4 samples = vec4("
"composite_sample(textureCoordinates[0], angles.x),"
"composite_sample(textureCoordinates[1], angles.y),"
"composite_sample(textureCoordinates[2], angles.z),"
"composite_sample(textureCoordinates[3], angles.w)"
");"
// Take the average to calculate luminance, then subtract that from all four samples to
// give chrominance.
"float luminance = dot(samples, vec4(0.25));"
"float chrominance = (dot(samples.yz, vec2(0.5)) - luminance) * oneOverCompositeAmplitude;"
// Pack that all up and send it on its way.
"fragColour = vec4(luminance, vec2(cos(compositeAngle), sin(compositeAngle)) * chrominance, 1.0);";
};
fragment_shader +=
"fragColour = fragColour*0.5 + vec4(0.5);"
"}";
return std::unique_ptr<Shader>(new Shader(
vertex_shader,
fragment_shader,
bindings(ShaderType::QAMSeparation)
));
}

13
Outputs/ScanTarget.cpp Normal file
View File

@ -0,0 +1,13 @@
//
// ScanTarget.cpp
// Clock Signal
//
// Created by Thomas Harte on 14/11/2018.
// Copyright © 2018 Thomas Harte. All rights reserved.
//
#include "ScanTarget.hpp"
using namespace Outputs::Display;
NullScanTarget NullScanTarget::singleton;

324
Outputs/ScanTarget.hpp Normal file
View File

@ -0,0 +1,324 @@
//
// ScanTarget.hpp
// Clock Signal
//
// Created by Thomas Harte on 30/10/2018.
// Copyright © 2018 Thomas Harte. All rights reserved.
//
#ifndef Outputs_Display_ScanTarget_h
#define Outputs_Display_ScanTarget_h
#include <cstddef>
#include <cstdint>
namespace Outputs {
namespace Display {
enum class Type {
PAL50,
NTSC60
};
struct Rect {
struct Point {
float x, y;
} origin;
struct {
float width, height;
} size;
Rect() : origin({0.0f, 0.0f}), size({1.0f, 1.0f}) {}
Rect(float x, float y, float width, float height) :
origin({x, y}), size({width, height}) {}
};
enum class ColourSpace {
/// YIQ is the NTSC colour space.
YIQ,
/// YUV is the PAL colour space.
YUV
};
enum class DisplayType {
RGB,
SVideo,
CompositeColour,
CompositeMonochrome
};
/*!
Enumerates the potential formats of input data.
*/
enum class InputDataType {
// The luminance types can be used to feed only two video pipelines:
// black and white video, or composite colour.
Luminance1, // 1 byte/pixel; any bit set => white; no bits set => black.
Luminance8, // 1 byte/pixel; linear scale.
PhaseLinkedLuminance8, // 4 bytes/pixel; each byte is an individual 8-bit luminance
// value and which value is output is a function of
// colour subcarrier phase — byte 0 defines the first quarter
// of each colour cycle, byte 1 the next quarter, etc. This
// format is intended to permit replay of sampled original data.
// The luminance plus phase types describe a luminance and the phase offset
// of a colour subcarrier. So they can be used to generate a luminance signal,
// or an s-video pipeline.
Luminance8Phase8, // 2 bytes/pixel; first is luminance, second is phase.
// Phase is encoded on a 192-unit circle; anything
// greater than 192 implies that the colour part of
// the signal should be omitted.
// The RGB types can directly feed an RGB pipeline, naturally, or can be mapped
// to phase+luminance, or just to luminance.
Red1Green1Blue1, // 1 byte/pixel; bit 0 is blue on or off, bit 1 is green, bit 2 is red.
Red2Green2Blue2, // 1 byte/pixel; bits 0 and 1 are blue, bits 2 and 3 are green, bits 4 and 5 are blue.
Red4Green4Blue4, // 2 bytes/pixel; first nibble is red, second is green, third is blue.
Red8Green8Blue8, // 4 bytes/pixel; first is red, second is green, third is blue, fourth is vacant.
};
inline size_t size_for_data_type(InputDataType data_type) {
switch(data_type) {
case InputDataType::Luminance1:
case InputDataType::Luminance8:
case InputDataType::Red1Green1Blue1:
case InputDataType::Red2Green2Blue2:
return 1;
case InputDataType::Luminance8Phase8:
case InputDataType::Red4Green4Blue4:
return 2;
case InputDataType::Red8Green8Blue8:
case InputDataType::PhaseLinkedLuminance8:
return 4;
default:
return 0;
}
}
inline DisplayType natural_display_type_for_data_type(InputDataType data_type) {
switch(data_type) {
default:
case InputDataType::Luminance1:
case InputDataType::Luminance8:
case InputDataType::PhaseLinkedLuminance8:
return DisplayType::CompositeColour;
case InputDataType::Red1Green1Blue1:
case InputDataType::Red2Green2Blue2:
case InputDataType::Red4Green4Blue4:
case InputDataType::Red8Green8Blue8:
return DisplayType::RGB;
case InputDataType::Luminance8Phase8:
return DisplayType::SVideo;
}
}
/*!
Provides an abstract target for 'scans' i.e. continuous sweeps of output data,
which are identified by 2d start and end coordinates, and the PCM-sampled data
that is output during the sweep.
Additional information is provided to allow decoding (and/or encoding) of a
composite colour feed.
Otherwise helpful: the ScanTarget vends all allocated memory. That should allow
for use of shared memory where available.
*/
struct ScanTarget {
/*
This top section of the interface deals with modal settings. A ScanTarget can
assume that the modals change very infrequently.
*/
struct Modals {
/// Describes the format of input data.
InputDataType input_data_type;
struct InputDataTweaks {
/// If using the PhaseLinkedLuminance8 data type, this value provides an offset
/// to add to phase before indexing the supplied luminances.
float phase_linked_luminance_offset = 0.0f;
} input_data_tweaks;
/// Describes the type of display that the data is being shown on.
DisplayType display_type = DisplayType::SVideo;
/// If being fed composite data, this defines the colour space in use.
ColourSpace composite_colour_space;
/// Provides an integral clock rate for the duration of "a single line", specifically
/// for an idealised line. So e.g. in NTSC this will be for the duration of 227.5
/// colour clocks, regardless of whether the source actually stretches lines to
/// 228 colour cycles, abbreviates them to 227 colour cycles, etc.
int cycles_per_line;
/// Sets a GCD for the durations of pixels coming out of this device. This with
/// the @c cycles_per_line are offered for sizing of intermediary buffers.
int clocks_per_pixel_greatest_common_divisor;
/// Provides the number of colour cycles in a line, as a quotient.
int colour_cycle_numerator, colour_cycle_denominator;
/// Provides a pre-estimate of the likely number of left-to-right scans per frame.
/// This isn't a guarantee, but it should provide a decent-enough estimate.
int expected_vertical_lines;
/// Provides an additional restriction on the section of the display that is expected
/// to contain interesting content.
Rect visible_area;
/// Describes the usual gamma of the output device these scans would appear on.
float intended_gamma = 2.2f;
/// Provides a brightness multiplier for the display output.
float brightness = 1.0f;
/// Specifies the range of values that will be output for x and y coordinates.
struct {
uint16_t x, y;
} output_scale;
};
/// Sets the total format of input data.
virtual void set_modals(Modals) = 0;
/*
This second section of the interface allows provision of the streamed data, plus some control
over the streaming.
*/
/*!
Defines a scan in terms of its two endpoints.
*/
struct Scan {
struct EndPoint {
/// Provide the coordinate of this endpoint. These are fixed point, purely fractional
/// numbers, relative to the scale provided in the Modals.
uint16_t x, y;
/// Provides the offset, in samples, into the most recently allocated write area, of data
/// at this end point.
uint16_t data_offset;
/// For composite video, provides the angle of the colour subcarrier at this endpoint.
///
/// This is a slightly weird fixed point, being:
///
/// * a six-bit fractional part;
/// * a nine-bit integral part; and
/// * a sign.
///
/// Positive numbers indicate that the colour subcarrier is 'running positively' on this
/// line; i.e. it is any NTSC line or an appropriate swing PAL line, encoded as
/// x*cos(a) + y*sin(a).
///
/// Negative numbers indicate a 'negative running' colour subcarrier; i.e. it is one of
/// the phase alternated lines of PAL, encoded as x*cos(a) - y*sin(a), or x*cos(-a) + y*sin(-a),
/// whichever you prefer.
///
/// It will produce undefined behaviour if signs differ on a single scan.
int16_t composite_angle;
/// Gives the number of cycles since the most recent horizontal retrace ended.
uint16_t cycles_since_end_of_horizontal_retrace;
} end_points[2];
/// For composite video, dictates the amplitude of the colour subcarrier as a proportion of
/// the whole, as determined from the colour burst. Will be 0 if there was no colour burst.
uint8_t composite_amplitude;
};
/// Requests a new scan to populate.
///
/// @return A valid pointer, or @c nullptr if insufficient further storage is available.
virtual Scan *begin_scan() = 0;
/// Requests a new scan to populate.
virtual void end_scan() {}
/// Finds the first available storage of at least @c required_length pixels in size which is
/// suitably aligned for writing of @c required_alignment number of samples at a time.
///
/// Calls will be paired off with calls to @c end_data.
///
/// @returns a pointer to the allocated space if any was available; @c nullptr otherwise.
virtual uint8_t *begin_data(size_t required_length, size_t required_alignment = 1) = 0;
/// Announces that the owner is finished with the region created by the most recent @c begin_data
/// and indicates that its actual final size was @c actual_length.
///
/// It is required that every call to begin_data be paired with a call to end_data.
virtual void end_data(size_t actual_length) {}
/// Tells the scan target that its owner is about to change; this is a hint that existing
/// data and scan allocations should be invalidated.
virtual void will_change_owner() {}
/// Marks the end of an atomic set of data. Drawing is best effort, so the scan target should either:
///
/// (i) output everything received since the previous submit; or
/// (ii) output nothing.
///
/// If there were any allocation failures — i.e. any nullptr responses to begin_data or
/// begin_scan — then (ii) is a required response. But a scan target may also need to opt for (ii)
/// for any other reason.
///
/// The ScanTarget isn't bound to take any drawing action immediately; it may sit on submitted data for
/// as long as it feels is appropriate subject to an @c flush.
virtual void submit() = 0;
/*
ScanTargets also receive notification of certain events that may be helpful in processing, particularly
for synchronising internal output to the outside world.
*/
enum class Event {
BeginHorizontalRetrace,
EndHorizontalRetrace,
BeginVerticalRetrace,
EndVerticalRetrace,
};
/*!
Provides a hint that the named event has occurred.
@param event The event.
@param is_visible @c true if the output stream is visible immediately after this event; @c false otherwise.
@param location The location of the event.
@param composite_amplitude The amplitude of the colour burst on this line (0, if no colour burst was found).
*/
virtual void announce(Event event, bool is_visible, const Scan::EndPoint &location, uint8_t composite_amplitude) {}
};
/*!
Provides a null target for scans.
*/
struct NullScanTarget: public ScanTarget {
void set_modals(Modals) {}
Scan *begin_scan() { return nullptr; }
uint8_t *begin_data(size_t required_length, size_t required_alignment = 1) { return nullptr; }
void submit() {}
static NullScanTarget singleton;
};
}
}
#endif /* Outputs_Display_ScanTarget_h */

View File

@ -45,7 +45,7 @@ enum Personality {
};
#define has_decimal_mode(p) ((p) >= Personality::P6502)
#define is_65c02(p) ((p) >= Personality::PSynertek65C02)
#define is_65c02(p) ((p) >= Personality::PSynertek65C02)
#define has_bbrbbsrmbsmb(p) ((p) >= Personality::PRockwell65C02)
#define has_stpwai(p) ((p) >= Personality::PWDC65C02)

View File

@ -114,6 +114,9 @@ FIRFilter::FIRFilter(std::size_t number_of_taps, float input_sample_rate, float
std::size_t Np = (number_of_taps - 1) / 2;
float two_over_sample_rate = 2.0f / input_sample_rate;
// Clamp the high cutoff frequency.
high_frequency = std::min(high_frequency, input_sample_rate * 0.5f);
std::vector<float> A(Np+1);
A[0] = 2.0f * (high_frequency - low_frequency) / input_sample_rate;
for(unsigned int i = 1; i <= Np; ++i) {
@ -146,6 +149,16 @@ FIRFilter FIRFilter::operator+(const FIRFilter &rhs) const {
return FIRFilter(sum);
}
FIRFilter FIRFilter::operator-() const {
std::vector<float> negative_coefficients;
for(const auto coefficient: get_coefficients()) {
negative_coefficients.push_back(1.0f - coefficient);
}
return FIRFilter(negative_coefficients);
}
FIRFilter FIRFilter::operator*(const FIRFilter &rhs) const {
std::vector<float> coefficients = get_coefficients();
std::vector<float> rhs_coefficients = rhs.get_coefficients();

View File

@ -29,6 +29,8 @@ class FIRFilter {
static constexpr int FixedShift = 15;
public:
/*! A suggested default attenuation value. */
constexpr static float DefaultAttenuation = 60.0f;
/*!
Creates an instance of @c FIRFilter.
@ -38,12 +40,9 @@ class FIRFilter {
@param high_frequency The highest frequency of signal to retain in the output.
@param attenuation The attenuation of the discarded frequencies.
*/
FIRFilter(std::size_t number_of_taps, float input_sample_rate, float low_frequency, float high_frequency, float attenuation);
FIRFilter(std::size_t number_of_taps, float input_sample_rate, float low_frequency, float high_frequency, float attenuation = DefaultAttenuation);
FIRFilter(const std::vector<float> &coefficients);
/*! A suggested default attenuation value. */
constexpr static float DefaultAttenuation = 60.0f;
/*!
Applies the filter to one batch of input samples, returning the net result.
@ -84,6 +83,11 @@ class FIRFilter {
*/
FIRFilter operator*(const FIRFilter &) const;
/*!
@returns A filter that would have the opposite effect of this filter.
*/
FIRFilter operator-() const;
private:
std::vector<short> filter_coefficients_;

View File

@ -265,7 +265,7 @@ void CPCDSK::set_tracks(const std::map<::Storage::Disk::Track::Address, std::sha
sector.fdc_status1 = 0;
sector.fdc_status2 = 0;
if(source_sector.second.has_data_crc_error) sector.fdc_status2 |= 0x20;
if(source_sector.second.has_data_crc_error) sector.fdc_status2 |= 0x20;
if(source_sector.second.has_header_crc_error) sector.fdc_status1 |= 0x20;
if(source_sector.second.is_deleted) sector.fdc_status2 |= 0x40;
}

View File

@ -19,6 +19,7 @@ using namespace Storage::Encodings::MFM;
class MFMEncoder: public Encoder {
public:
MFMEncoder(std::vector<bool> &target) : Encoder(target) {}
virtual ~MFMEncoder() {}
void add_byte(uint8_t input) {
crc_generator_.add(input);

View File

@ -45,6 +45,7 @@ std::shared_ptr<Storage::Disk::Track> GetFMTrackWithSectors(const std::vector<co
class Encoder {
public:
Encoder(std::vector<bool> &target);
virtual ~Encoder() {}
virtual void add_byte(uint8_t input) = 0;
virtual void add_index_address_mark() = 0;
virtual void add_ID_address_mark() = 0;

View File

@ -70,6 +70,8 @@ class HeadPosition {
*/
class Track {
public:
virtual ~Track() {}
/*!
Describes the location of a track, implementing < to allow for use as a set key.
*/

View File

@ -155,7 +155,7 @@ bool FileHolder::check_signature(const char *signature, std::size_t length) {
// read and check the file signature
std::vector<uint8_t> stored_signature = read(length);
if(stored_signature.size() != length) return false;
if(std::memcmp(stored_signature.data(), signature, length)) return false;
if(std::memcmp(stored_signature.data(), signature, length)) return false;
return true;
}

View File

@ -49,7 +49,7 @@ class CAS: public Tape {
// each chunk is preceded by a header which may be long, and is optionally
// also preceded by a gap.
struct Chunk {
bool has_gap;
bool has_gap;
bool long_header;
std::vector<std::uint8_t> data;
};

View File

@ -12,6 +12,8 @@
#include <cstdlib>
#include <cmath>
#include "../../../Outputs/Log.hpp"
// MARK: - ZLib extensions
static float gzgetfloat(gzFile file) {
@ -153,7 +155,7 @@ void UEF::get_next_pulses() {
break;
default:
printf("!!! Skipping %04x\n", next_chunk.id);
LOG("!!! Skipping " << std::hex << next_chunk.id << std::endl);
break;
}

View File

@ -67,7 +67,7 @@ std::unique_ptr<Parser::FileSpeed> Parser::find_header(Storage::Tape::BinaryTape
of a 0 start bit. The HI cycle length is placed in WINWID and will be used to discriminate
between LO and HI cycles."
*/
total_length = total_length / 256.0f; // To get the average, in microseconds.
total_length = total_length / 256.0f; // To get the average, in microseconds.
// To convert to the loop count format used by the MSX BIOS.
uint8_t int_result = static_cast<uint8_t>(total_length / (0.00001145f * 0.75f));