mirror of
https://github.com/TomHarte/CLK.git
synced 2025-04-06 10:38:16 +00:00
Made an attempt to chop out all the stuff of building up the OpenGL data from the stuff of parsing input.
This commit is contained in:
parent
14b2927275
commit
bf5747f83e
@ -664,6 +664,7 @@
|
||||
4BBF99111C8FBA6F0075DAFB /* Shader.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Shader.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>"; };
|
||||
4BE5F85C1C3E1C2500C43F01 /* basic.rom */ = {isa = PBXFileReference; lastKnownFileType = file; path = basic.rom; sourceTree = "<group>"; };
|
||||
4BE5F85D1C3E1C2500C43F01 /* os.rom */ = {isa = PBXFileReference; lastKnownFileType = file; path = os.rom; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@ -700,6 +701,7 @@
|
||||
4BBF99071C8FBA6F0075DAFB /* Internals */,
|
||||
4B0CCC421C62D0B3001CAC5F /* CRT.cpp */,
|
||||
4B0CCC431C62D0B3001CAC5F /* CRT.hpp */,
|
||||
4BBF99191C8FC2750075DAFB /* CRTTypes.hpp */,
|
||||
);
|
||||
name = CRT;
|
||||
path = ../../Outputs/CRT;
|
||||
|
@ -15,9 +15,7 @@ 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)
|
||||
{
|
||||
_colour_space = colour_space;
|
||||
_colour_cycle_numerator = colour_cycle_numerator;
|
||||
_colour_cycle_denominator = colour_cycle_denominator;
|
||||
_openGL_output_builder->set_colour_format(colour_space, colour_cycle_numerator, colour_cycle_denominator);
|
||||
|
||||
const unsigned int syncCapacityLineChargeThreshold = 3;
|
||||
const unsigned int millisecondsHorizontalRetraceTime = 7; // source: Dictionary of Video and Television Technology, p. 234
|
||||
@ -45,6 +43,8 @@ void CRT::set_new_timing(unsigned int cycles_per_line, unsigned int height_of_di
|
||||
// figure out the divisor necessary to get the horizontal flywheel into a 16-bit range
|
||||
unsigned int real_clock_scan_period = (_cycles_per_line * height_of_display) / (_time_multiplier * _common_output_divisor);
|
||||
_vertical_flywheel_output_divider = (uint16_t)(ceilf(real_clock_scan_period / 65536.0f) * (_time_multiplier * _common_output_divisor));
|
||||
|
||||
_openGL_output_builder->set_timing(_cycles_per_line, _height_of_display, _horizontal_flywheel->get_scan_period(), _vertical_flywheel->get_scan_period(), _vertical_flywheel_output_divider);
|
||||
}
|
||||
|
||||
void CRT::set_new_display_type(unsigned int cycles_per_line, DisplayType displayType)
|
||||
@ -61,63 +61,31 @@ void CRT::set_new_display_type(unsigned int cycles_per_line, DisplayType display
|
||||
}
|
||||
}
|
||||
|
||||
void CRT::allocate_buffers(unsigned int number, va_list sizes)
|
||||
{
|
||||
_run_builders = new CRTRunBuilder *[NumberOfFields];
|
||||
for(int builder = 0; builder < NumberOfFields; builder++)
|
||||
{
|
||||
_run_builders[builder] = new CRTRunBuilder(OutputVertexSize);
|
||||
}
|
||||
_composite_src_runs = std::unique_ptr<CRTRunBuilder>(new CRTRunBuilder(InputVertexSize));
|
||||
|
||||
va_list va;
|
||||
va_copy(va, sizes);
|
||||
_buffer_builder = std::unique_ptr<CRTInputBufferBuilder>(new CRTInputBufferBuilder(number, va));
|
||||
va_end(va);
|
||||
}
|
||||
|
||||
CRT::CRT(unsigned int common_output_divisor) :
|
||||
_run_write_pointer(0),
|
||||
_sync_capacitor_charge_level(0),
|
||||
_is_receiving_sync(false),
|
||||
_output_mutex(new std::mutex),
|
||||
_visible_area(Rect(0, 0, 1, 1)),
|
||||
_sync_period(0),
|
||||
_common_output_divisor(common_output_divisor),
|
||||
_composite_src_output_y(0),
|
||||
_is_writing_composite_run(false)
|
||||
{
|
||||
construct_openGL();
|
||||
}
|
||||
|
||||
CRT::~CRT()
|
||||
{
|
||||
for(int builder = 0; builder < NumberOfFields; builder++)
|
||||
{
|
||||
delete _run_builders[builder];
|
||||
}
|
||||
delete[] _run_builders;
|
||||
destruct_openGL();
|
||||
}
|
||||
_is_writing_composite_run(false) {}
|
||||
|
||||
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 number_of_buffers, ...) : CRT(common_output_divisor)
|
||||
{
|
||||
set_new_timing(cycles_per_line, height_of_display, colour_space, colour_cycle_numerator, colour_cycle_denominator);
|
||||
|
||||
va_list buffer_sizes;
|
||||
va_start(buffer_sizes, number_of_buffers);
|
||||
allocate_buffers(number_of_buffers, buffer_sizes);
|
||||
_openGL_output_builder = std::unique_ptr<OpenGLOutputBuilder>(new OpenGLOutputBuilder(number_of_buffers, buffer_sizes));
|
||||
va_end(buffer_sizes);
|
||||
|
||||
set_new_timing(cycles_per_line, height_of_display, colour_space, colour_cycle_numerator, colour_cycle_denominator);
|
||||
}
|
||||
|
||||
CRT::CRT(unsigned int cycles_per_line, unsigned int common_output_divisor, DisplayType displayType, unsigned int number_of_buffers, ...) : CRT(common_output_divisor)
|
||||
{
|
||||
set_new_display_type(cycles_per_line, displayType);
|
||||
|
||||
va_list buffer_sizes;
|
||||
va_start(buffer_sizes, number_of_buffers);
|
||||
allocate_buffers(number_of_buffers, buffer_sizes);
|
||||
_openGL_output_builder = std::unique_ptr<OpenGLOutputBuilder>(new OpenGLOutputBuilder(number_of_buffers, buffer_sizes));
|
||||
va_end(buffer_sizes);
|
||||
|
||||
set_new_display_type(cycles_per_line, displayType);
|
||||
}
|
||||
|
||||
#pragma mark - Sync loop
|
||||
@ -147,11 +115,11 @@ Flywheel::SyncEvent CRT::get_next_horizontal_sync_event(bool hsync_is_requested,
|
||||
#define input_amplitude(v) next_run[OutputVertexSize*v + InputVertexOffsetOfPhaseAndAmplitude + 1]
|
||||
#define input_phase_time(v) (*(uint16_t *)&next_run[OutputVertexSize*v + InputVertexOffsetOfPhaseTime])
|
||||
|
||||
void CRT::advance_cycles(unsigned int number_of_cycles, unsigned int source_divider, bool hsync_requested, bool vsync_requested, const bool vsync_charging, const Type type, uint16_t tex_x, uint16_t tex_y)
|
||||
void CRT::advance_cycles(unsigned int number_of_cycles, unsigned int source_divider, bool hsync_requested, bool vsync_requested, const bool vsync_charging, const Scan::Type type, uint16_t tex_x, uint16_t tex_y)
|
||||
{
|
||||
number_of_cycles *= _time_multiplier;
|
||||
|
||||
bool is_output_run = ((type == Type::Level) || (type == Type::Data));
|
||||
bool is_output_run = ((type == Scan::Type::Level) || (type == Scan::Type::Data));
|
||||
|
||||
while(number_of_cycles) {
|
||||
|
||||
@ -170,8 +138,7 @@ void CRT::advance_cycles(unsigned int number_of_cycles, unsigned int source_divi
|
||||
uint8_t *next_run = nullptr;
|
||||
if(is_output_segment)
|
||||
{
|
||||
_output_mutex->lock();
|
||||
next_run = (_output_device == Monitor) ? _run_builders[_run_write_pointer]->get_next_run(6) : _composite_src_runs->get_next_run(2);
|
||||
next_run = _openGL_output_builder->get_next_input_run();
|
||||
}
|
||||
|
||||
// Vertex output is arranged for triangle strips, as:
|
||||
@ -181,12 +148,12 @@ void CRT::advance_cycles(unsigned int number_of_cycles, unsigned int source_divi
|
||||
// [0/1] 3
|
||||
if(next_run)
|
||||
{
|
||||
if(_output_device == Monitor)
|
||||
if(_openGL_output_builder->get_output_device() == Monitor)
|
||||
{
|
||||
// set the type, initial raster position and type of this run
|
||||
output_position_x(0) = output_position_x(1) = output_position_x(2) = (uint16_t)_horizontal_flywheel->get_current_output_position();
|
||||
output_position_y(0) = output_position_y(1) = output_position_y(2) = (uint16_t)(_vertical_flywheel->get_current_output_position() / _vertical_flywheel_output_divider);
|
||||
output_timestamp(0) = output_timestamp(1) = output_timestamp(2) = _run_builders[_run_write_pointer]->duration;
|
||||
output_timestamp(0) = output_timestamp(1) = output_timestamp(2) = _openGL_output_builder->get_current_field_time();
|
||||
output_tex_x(0) = output_tex_x(1) = output_tex_x(2) = tex_x;
|
||||
|
||||
// these things are constants across the line so just throw them out now
|
||||
@ -199,7 +166,7 @@ void CRT::advance_cycles(unsigned int number_of_cycles, unsigned int source_divi
|
||||
input_input_position_x(0) = tex_x;
|
||||
input_input_position_y(0) = input_input_position_y(1) = tex_y;
|
||||
input_output_position_x(0) = (uint16_t)_horizontal_flywheel->get_current_output_position();
|
||||
input_output_position_y(0) = input_output_position_y(1) = _composite_src_output_y;
|
||||
input_output_position_y(0) = input_output_position_y(1) = _openGL_output_builder->get_composite_output_y();
|
||||
input_phase(0) = input_phase(1) = _colour_burst_phase;
|
||||
input_amplitude(0) = input_amplitude(1) = _colour_burst_amplitude;
|
||||
input_phase_time(0) = input_phase_time(1) = _colour_burst_time;
|
||||
@ -209,7 +176,7 @@ void CRT::advance_cycles(unsigned int number_of_cycles, unsigned int source_divi
|
||||
// decrement the number of cycles left to run for and increment the
|
||||
// horizontal counter appropriately
|
||||
number_of_cycles -= next_run_length;
|
||||
_run_builders[_run_write_pointer]->duration += next_run_length;
|
||||
_openGL_output_builder->add_to_field_time(next_run_length);
|
||||
|
||||
// either charge or deplete the vertical retrace capacitor (making sure it stops at 0)
|
||||
if (vsync_charging && !_vertical_flywheel->is_in_retrace())
|
||||
@ -224,14 +191,14 @@ void CRT::advance_cycles(unsigned int number_of_cycles, unsigned int source_divi
|
||||
if(next_run)
|
||||
{
|
||||
// if this is a data run then advance the buffer pointer
|
||||
if(type == Type::Data && source_divider) tex_x += next_run_length / (_time_multiplier * source_divider);
|
||||
if(type == Scan::Type::Data && source_divider) tex_x += next_run_length / (_time_multiplier * source_divider);
|
||||
|
||||
if(_output_device == Monitor)
|
||||
if(_openGL_output_builder->get_output_device() == Monitor)
|
||||
{
|
||||
// store the final raster position
|
||||
output_position_x(3) = output_position_x(4) = output_position_x(5) = (uint16_t)_horizontal_flywheel->get_current_output_position();
|
||||
output_position_y(3) = output_position_y(4) = output_position_y(5) = (uint16_t)(_vertical_flywheel->get_current_output_position() / _vertical_flywheel_output_divider);
|
||||
output_timestamp(3) = output_timestamp(4) = output_timestamp(5) = _run_builders[_run_write_pointer]->duration;
|
||||
output_timestamp(3) = output_timestamp(4) = output_timestamp(5) = _openGL_output_builder->get_current_field_time();
|
||||
output_tex_x(3) = output_tex_x(4) = output_tex_x(5) = tex_x;
|
||||
}
|
||||
else
|
||||
@ -243,11 +210,11 @@ void CRT::advance_cycles(unsigned int number_of_cycles, unsigned int source_divi
|
||||
|
||||
if(is_output_segment)
|
||||
{
|
||||
_output_mutex->unlock();
|
||||
_openGL_output_builder->complete_input_run();
|
||||
}
|
||||
|
||||
// if this is horizontal retrace then advance the output line counter and bookend an output run
|
||||
if(_output_device == Television)
|
||||
if(_openGL_output_builder->get_output_device() == Television)
|
||||
{
|
||||
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;
|
||||
@ -258,23 +225,24 @@ void CRT::advance_cycles(unsigned int number_of_cycles, unsigned int source_divi
|
||||
|
||||
if(needs_endpoint)
|
||||
{
|
||||
uint8_t *next_run = _run_builders[_run_write_pointer]->get_next_run(3);
|
||||
uint8_t *next_run = _openGL_output_builder->get_next_output_run();
|
||||
|
||||
output_position_x(0) = output_position_x(1) = output_position_x(2) = (uint16_t)_horizontal_flywheel->get_current_output_position();
|
||||
output_position_y(0) = output_position_y(1) = output_position_y(2) = (uint16_t)(_vertical_flywheel->get_current_output_position() / _vertical_flywheel_output_divider);
|
||||
output_timestamp(0) = output_timestamp(1) = output_timestamp(2) = _run_builders[_run_write_pointer]->duration;
|
||||
output_timestamp(0) = output_timestamp(1) = output_timestamp(2) = _openGL_output_builder->get_current_field_time();
|
||||
output_tex_x(0) = output_tex_x(1) = output_tex_x(2) = (uint16_t)_horizontal_flywheel->get_current_output_position();
|
||||
output_tex_y(0) = output_tex_y(1) = output_tex_y(2) = _composite_src_output_y;
|
||||
output_tex_y(0) = output_tex_y(1) = output_tex_y(2) = _openGL_output_builder->get_composite_output_y();
|
||||
output_lateral(0) = 0;
|
||||
output_lateral(1) = _is_writing_composite_run ? 1 : 0;
|
||||
output_lateral(2) = 1;
|
||||
|
||||
_openGL_output_builder->complete_output_run();
|
||||
_is_writing_composite_run ^= true;
|
||||
}
|
||||
|
||||
if(next_run_length == time_until_horizontal_sync_event && next_horizontal_sync_event == Flywheel::SyncEvent::EndRetrace)
|
||||
{
|
||||
_composite_src_output_y = (_composite_src_output_y + 1) % IntermediateBufferHeight;
|
||||
_openGL_output_builder->increment_composite_output_y();
|
||||
}
|
||||
}
|
||||
|
||||
@ -284,8 +252,7 @@ void CRT::advance_cycles(unsigned int number_of_cycles, unsigned int source_divi
|
||||
// TODO: how to communicate did_detect_vsync? Bring the delegate back?
|
||||
// _delegate->crt_did_end_frame(this, &_current_frame_builder->frame, _did_detect_vsync);
|
||||
|
||||
_run_write_pointer = (_run_write_pointer + 1)%NumberOfFields;
|
||||
_run_builders[_run_write_pointer]->reset();
|
||||
_openGL_output_builder->increment_field();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -309,14 +276,14 @@ void CRT::advance_cycles(unsigned int number_of_cycles, unsigned int source_divi
|
||||
|
||||
void CRT::output_scan(Scan *scan)
|
||||
{
|
||||
bool this_is_sync = (scan->type == Type::Sync);
|
||||
bool this_is_sync = (scan->type == Scan::Type::Sync);
|
||||
bool is_trailing_edge = (_is_receiving_sync && !this_is_sync);
|
||||
bool hsync_requested = is_trailing_edge && (_sync_period < (_horizontal_flywheel->get_scan_period() >> 2));
|
||||
bool vsync_requested = is_trailing_edge && (_sync_capacitor_charge_level >= _sync_capacitor_charge_threshold);
|
||||
_is_receiving_sync = this_is_sync;
|
||||
|
||||
// simplified colour burst logic: if it's within the back porch we'll take it
|
||||
if(scan->type == Type::ColourBurst)
|
||||
if(scan->type == Scan::Type::ColourBurst)
|
||||
{
|
||||
if(_horizontal_flywheel->get_current_time() < (_horizontal_flywheel->get_standard_period() * 12) >> 6)
|
||||
{
|
||||
@ -338,7 +305,7 @@ void CRT::output_scan(Scan *scan)
|
||||
void CRT::output_sync(unsigned int number_of_cycles)
|
||||
{
|
||||
Scan scan{
|
||||
.type = Type::Sync,
|
||||
.type = Scan::Type::Sync,
|
||||
.number_of_cycles = number_of_cycles
|
||||
};
|
||||
output_scan(&scan);
|
||||
@ -347,7 +314,7 @@ void CRT::output_sync(unsigned int number_of_cycles)
|
||||
void CRT::output_blank(unsigned int number_of_cycles)
|
||||
{
|
||||
Scan scan {
|
||||
.type = Type::Blank,
|
||||
.type = Scan::Type::Blank,
|
||||
.number_of_cycles = number_of_cycles
|
||||
};
|
||||
output_scan(&scan);
|
||||
@ -356,10 +323,10 @@ void CRT::output_blank(unsigned int number_of_cycles)
|
||||
void CRT::output_level(unsigned int number_of_cycles)
|
||||
{
|
||||
Scan scan {
|
||||
.type = Type::Level,
|
||||
.type = Scan::Type::Level,
|
||||
.number_of_cycles = number_of_cycles,
|
||||
.tex_x = _buffer_builder->_write_x_position,
|
||||
.tex_y = _buffer_builder->_write_y_position
|
||||
.tex_x = _openGL_output_builder->get_last_write_x_posiiton(),
|
||||
.tex_y = _openGL_output_builder->get_last_write_y_posiiton()
|
||||
};
|
||||
output_scan(&scan);
|
||||
}
|
||||
@ -367,7 +334,7 @@ void CRT::output_level(unsigned int number_of_cycles)
|
||||
void CRT::output_colour_burst(unsigned int number_of_cycles, uint8_t phase, uint8_t amplitude)
|
||||
{
|
||||
Scan scan {
|
||||
.type = Type::ColourBurst,
|
||||
.type = Scan::Type::ColourBurst,
|
||||
.number_of_cycles = number_of_cycles,
|
||||
.phase = phase,
|
||||
.amplitude = amplitude
|
||||
@ -377,27 +344,13 @@ void CRT::output_colour_burst(unsigned int number_of_cycles, uint8_t phase, uint
|
||||
|
||||
void CRT::output_data(unsigned int number_of_cycles, unsigned int source_divider)
|
||||
{
|
||||
_buffer_builder->reduce_previous_allocation_to(number_of_cycles / source_divider);
|
||||
_openGL_output_builder->reduce_previous_allocation_to(number_of_cycles / source_divider);
|
||||
Scan scan {
|
||||
.type = Type::Data,
|
||||
.type = Scan::Type::Data,
|
||||
.number_of_cycles = number_of_cycles,
|
||||
.tex_x = _buffer_builder->_write_x_position,
|
||||
.tex_y = _buffer_builder->_write_y_position,
|
||||
.tex_x = _openGL_output_builder->get_last_write_x_posiiton(),
|
||||
.tex_y = _openGL_output_builder->get_last_write_y_posiiton(),
|
||||
.source_divider = source_divider
|
||||
};
|
||||
output_scan(&scan);
|
||||
}
|
||||
|
||||
#pragma mark - Buffer supply
|
||||
|
||||
void CRT::allocate_write_area(size_t required_length)
|
||||
{
|
||||
_output_mutex->lock();
|
||||
_buffer_builder->allocate_write_area(required_length);
|
||||
_output_mutex->unlock();
|
||||
}
|
||||
|
||||
uint8_t *CRT::get_write_target_for_buffer(int buffer)
|
||||
{
|
||||
return _buffer_builder->get_write_target_for_buffer(buffer);
|
||||
}
|
||||
|
@ -15,48 +15,70 @@
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
|
||||
#include "CRTTypes.hpp"
|
||||
#include "Internals/Flywheel.hpp"
|
||||
#include "Internals/CRTInputBufferBuilder.hpp"
|
||||
#include "Internals/CRTRunBuilder.hpp"
|
||||
#include "Internals/CRTOpenGL.hpp"
|
||||
|
||||
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 = x, .y = y}), size({.width = width, .height =height}) {}
|
||||
};
|
||||
|
||||
enum DisplayType {
|
||||
PAL50,
|
||||
NTSC60
|
||||
};
|
||||
|
||||
enum ColourSpace {
|
||||
YIQ,
|
||||
YUV
|
||||
};
|
||||
|
||||
enum OutputDevice {
|
||||
Monitor,
|
||||
Television
|
||||
};
|
||||
|
||||
struct OpenGLState;
|
||||
|
||||
class CRT {
|
||||
public:
|
||||
~CRT();
|
||||
private:
|
||||
CRT(unsigned int common_output_divisor);
|
||||
|
||||
// the incoming clock lengths will be multiplied by something to give at least 1000
|
||||
// sample points per line
|
||||
unsigned int _time_multiplier;
|
||||
const unsigned int _common_output_divisor;
|
||||
|
||||
// fundamental creator-specified properties
|
||||
unsigned int _cycles_per_line;
|
||||
unsigned int _height_of_display;
|
||||
|
||||
// the two flywheels regulating scanning
|
||||
std::unique_ptr<Flywheel> _horizontal_flywheel, _vertical_flywheel;
|
||||
uint16_t _vertical_flywheel_output_divider;
|
||||
|
||||
// elements of sync separation
|
||||
bool _is_receiving_sync; // true if the CRT is currently receiving sync (i.e. this is for edge triggering of horizontal sync)
|
||||
int _sync_capacitor_charge_level; // this charges up during times of sync and depletes otherwise; needs to hit a required threshold to trigger a vertical sync
|
||||
int _sync_capacitor_charge_threshold; // this charges up during times of sync and depletes otherwise; needs to hit a required threshold to trigger a vertical sync
|
||||
unsigned int _sync_period;
|
||||
|
||||
// each call to output_* generates a scan. A two-slot queue for scans allows edge extensions.
|
||||
struct Scan {
|
||||
enum Type {
|
||||
Sync, Level, Data, Blank, ColourBurst
|
||||
} type;
|
||||
unsigned int number_of_cycles;
|
||||
union {
|
||||
struct {
|
||||
unsigned int source_divider;
|
||||
uint16_t tex_x, tex_y;
|
||||
};
|
||||
struct {
|
||||
uint8_t phase, amplitude;
|
||||
};
|
||||
};
|
||||
};
|
||||
void output_scan(Scan *scan);
|
||||
|
||||
uint8_t _colour_burst_phase, _colour_burst_amplitude;
|
||||
uint16_t _colour_burst_time;
|
||||
bool _is_writing_composite_run;
|
||||
|
||||
// the outer entry point for dispatching output_sync, output_blank, output_level and output_data
|
||||
void advance_cycles(unsigned int number_of_cycles, unsigned int source_divider, bool hsync_requested, bool vsync_requested, const bool vsync_charging, const Scan::Type type, uint16_t tex_x, uint16_t tex_y);
|
||||
|
||||
// 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, kept behind an opaque pointer to avoid inclusion of the GL headers here.
|
||||
std::unique_ptr<OpenGLOutputBuilder> _openGL_output_builder;
|
||||
|
||||
public:
|
||||
/*! Constructs the CRT with a specified clock rate, height and colour subcarrier frequency.
|
||||
The requested number of buffers, each with the requested number of bytes per pixel,
|
||||
is created for the machine to write raw pixel data to.
|
||||
@ -162,19 +184,28 @@ class CRT {
|
||||
|
||||
@param required_length The number of samples to allocate.
|
||||
*/
|
||||
void allocate_write_area(size_t required_length);
|
||||
inline void allocate_write_area(size_t required_length)
|
||||
{
|
||||
return _openGL_output_builder->allocate_write_area(required_length);
|
||||
}
|
||||
|
||||
/*! Gets a pointer for writing to the area created by the most recent call to @c allocate_write_area
|
||||
for the nominated buffer.
|
||||
|
||||
@param buffer The buffer to get a write target for.
|
||||
*/
|
||||
uint8_t *get_write_target_for_buffer(int buffer);
|
||||
inline uint8_t *get_write_target_for_buffer(int buffer)
|
||||
{
|
||||
return _openGL_output_builder->get_write_target_for_buffer(buffer);
|
||||
}
|
||||
|
||||
/*! 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.
|
||||
*/
|
||||
void draw_frame(unsigned int output_width, unsigned int output_height, bool only_if_dirty);
|
||||
inline void draw_frame(unsigned int output_width, unsigned int output_height, bool only_if_dirty)
|
||||
{
|
||||
_openGL_output_builder->draw_frame(output_width, output_height, only_if_dirty);
|
||||
}
|
||||
|
||||
/*! Tells the CRT that the next call to draw_frame will occur on a different OpenGL context than
|
||||
the previous.
|
||||
@ -183,7 +214,10 @@ class CRT {
|
||||
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.
|
||||
*/
|
||||
void set_openGL_context_will_change(bool should_delete_resources);
|
||||
inline void set_openGL_context_will_change(bool should_delete_resources)
|
||||
{
|
||||
_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.
|
||||
|
||||
@ -192,7 +226,10 @@ class CRT {
|
||||
level as a function of a source buffer sampling location and the provided colour carrier phase.
|
||||
The shader may assume a uniform array of sampler2Ds named `buffers` provides access to all input data.
|
||||
*/
|
||||
void set_composite_sampling_function(const char *shader);
|
||||
inline void set_composite_sampling_function(const char *shader)
|
||||
{
|
||||
_openGL_output_builder->set_composite_sampling_function(shader);
|
||||
}
|
||||
|
||||
/*! Sets a function that will map from whatever data the machine provided to an RGB signal.
|
||||
|
||||
@ -204,7 +241,10 @@ class CRT {
|
||||
the source buffer sampling location.
|
||||
The shader may assume a uniform array of sampler2Ds named `buffers` provides access to all input data.
|
||||
*/
|
||||
void set_rgb_sampling_function(const char *shader);
|
||||
inline void set_rgb_sampling_function(const char *shader)
|
||||
{
|
||||
_openGL_output_builder->set_rgb_sampling_function(shader);
|
||||
}
|
||||
|
||||
/*! Optionally sets a function that will map from an input cycle count to a colour carrier phase.
|
||||
|
||||
@ -218,128 +258,15 @@ class CRT {
|
||||
*/
|
||||
// void set_phase_function(const char *shader);
|
||||
|
||||
void set_output_device(OutputDevice output_device);
|
||||
void set_visible_area(Rect visible_area)
|
||||
inline void set_output_device(OutputDevice output_device)
|
||||
{
|
||||
_visible_area = visible_area;
|
||||
_openGL_output_builder->set_output_device(output_device);
|
||||
}
|
||||
|
||||
#ifdef DEBUG
|
||||
inline uint32_t get_field_cycle()
|
||||
inline void set_visible_area(Rect visible_area)
|
||||
{
|
||||
return _run_builders[_run_write_pointer]->duration / _time_multiplier;
|
||||
_openGL_output_builder->set_visible_area(visible_area);
|
||||
}
|
||||
|
||||
inline uint32_t get_line_cycle()
|
||||
{
|
||||
return _horizontal_flywheel->get_current_time() / _time_multiplier;
|
||||
}
|
||||
|
||||
inline float get_raster_x()
|
||||
{
|
||||
return (float)_horizontal_flywheel->get_current_output_position() / (float)_horizontal_flywheel->get_scan_period();
|
||||
}
|
||||
#endif
|
||||
|
||||
private:
|
||||
CRT(unsigned int common_output_divisor);
|
||||
void allocate_buffers(unsigned int number, va_list sizes);
|
||||
|
||||
// the incoming clock lengths will be multiplied by something to give at least 1000
|
||||
// sample points per line
|
||||
unsigned int _time_multiplier;
|
||||
const unsigned int _common_output_divisor;
|
||||
|
||||
// fundamental creator-specified properties
|
||||
unsigned int _cycles_per_line;
|
||||
unsigned int _height_of_display;
|
||||
|
||||
// colour invormation
|
||||
ColourSpace _colour_space;
|
||||
unsigned int _colour_cycle_numerator;
|
||||
unsigned int _colour_cycle_denominator;
|
||||
OutputDevice _output_device;
|
||||
|
||||
// The user-supplied visible area
|
||||
Rect _visible_area;
|
||||
|
||||
// the two flywheels regulating scanning
|
||||
std::unique_ptr<Flywheel> _horizontal_flywheel, _vertical_flywheel;
|
||||
uint16_t _vertical_flywheel_output_divider;
|
||||
|
||||
// elements of sync separation
|
||||
bool _is_receiving_sync; // true if the CRT is currently receiving sync (i.e. this is for edge triggering of horizontal sync)
|
||||
int _sync_capacitor_charge_level; // this charges up during times of sync and depletes otherwise; needs to hit a required threshold to trigger a vertical sync
|
||||
int _sync_capacitor_charge_threshold; // this charges up during times of sync and depletes otherwise; needs to hit a required threshold to trigger a vertical sync
|
||||
unsigned int _sync_period;
|
||||
|
||||
// the outer entry point for dispatching output_sync, output_blank, output_level and output_data
|
||||
enum Type {
|
||||
Sync, Level, Data, Blank, ColourBurst
|
||||
} type;
|
||||
void advance_cycles(unsigned int number_of_cycles, unsigned int source_divider, bool hsync_requested, bool vsync_requested, const bool vsync_charging, const Type type, uint16_t tex_x, uint16_t tex_y);
|
||||
|
||||
// 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);
|
||||
|
||||
// each call to output_* generates a scan. A two-slot queue for scans allows edge extensions.
|
||||
struct Scan {
|
||||
Type type;
|
||||
unsigned int number_of_cycles;
|
||||
union {
|
||||
struct {
|
||||
unsigned int source_divider;
|
||||
uint16_t tex_x, tex_y;
|
||||
};
|
||||
struct {
|
||||
uint8_t phase, amplitude;
|
||||
};
|
||||
};
|
||||
};
|
||||
void output_scan(Scan *scan);
|
||||
|
||||
// the run and input data buffers
|
||||
std::unique_ptr<CRTInputBufferBuilder> _buffer_builder;
|
||||
CRTRunBuilder **_run_builders;
|
||||
int _run_write_pointer;
|
||||
std::shared_ptr<std::mutex> _output_mutex;
|
||||
|
||||
// transient buffers indicating composite data not yet decoded
|
||||
std::unique_ptr<CRTRunBuilder> _composite_src_runs;
|
||||
uint16_t _composite_src_output_y;
|
||||
uint8_t _colour_burst_phase, _colour_burst_amplitude;
|
||||
uint16_t _colour_burst_time;
|
||||
bool _is_writing_composite_run;
|
||||
|
||||
// OpenGL state, kept behind an opaque pointer to avoid inclusion of the GL headers here.
|
||||
OpenGLState *_openGL_state;
|
||||
|
||||
// Other things the caller may have provided.
|
||||
char *_composite_shader;
|
||||
char *_rgb_shader;
|
||||
|
||||
// Setup and teardown for the OpenGL code
|
||||
void construct_openGL();
|
||||
void destruct_openGL();
|
||||
|
||||
// Methods used by the OpenGL code
|
||||
void prepare_rgb_output_shader();
|
||||
void prepare_composite_input_shader();
|
||||
void prepare_output_vertex_array();
|
||||
void push_size_uniforms(unsigned int output_width, unsigned int output_height);
|
||||
|
||||
char *get_output_vertex_shader();
|
||||
|
||||
char *get_output_fragment_shader(const char *sampling_function);
|
||||
char *get_rgb_output_fragment_shader();
|
||||
char *get_composite_output_fragment_shader();
|
||||
|
||||
char *get_input_vertex_shader();
|
||||
char *get_input_fragment_shader();
|
||||
|
||||
char *get_compound_shader(const char *base, const char *insert);
|
||||
};
|
||||
|
||||
}
|
||||
|
47
Outputs/CRT/CRTTypes.hpp
Normal file
47
Outputs/CRT/CRTTypes.hpp
Normal file
@ -0,0 +1,47 @@
|
||||
//
|
||||
// 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 = x, .y = y}), size({.width = width, .height =height}) {}
|
||||
};
|
||||
|
||||
enum DisplayType {
|
||||
PAL50,
|
||||
NTSC60
|
||||
};
|
||||
|
||||
enum ColourSpace {
|
||||
YIQ,
|
||||
YUV
|
||||
};
|
||||
|
||||
enum OutputDevice {
|
||||
Monitor,
|
||||
Television
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* CRTTypes_h */
|
@ -9,42 +9,47 @@
|
||||
#include <stdlib.h>
|
||||
#include <math.h>
|
||||
|
||||
#include "OpenGL.hpp"
|
||||
#include "TextureTarget.hpp"
|
||||
#include "Shader.hpp"
|
||||
#include "CRTOpenGL.hpp"
|
||||
|
||||
namespace Outputs {
|
||||
namespace CRT {
|
||||
|
||||
struct OpenGLState {
|
||||
std::unique_ptr<OpenGL::Shader> rgb_shader_program;
|
||||
std::unique_ptr<OpenGL::Shader> composite_input_shader_program, composite_output_shader_program;
|
||||
|
||||
GLuint output_array_buffer, output_vertex_array;
|
||||
size_t output_vertices_per_slice;
|
||||
|
||||
GLint windowSizeUniform, timestampBaseUniform;
|
||||
GLint boundsOriginUniform, boundsSizeUniform;
|
||||
|
||||
GLuint textureName, shadowMaskTextureName;
|
||||
|
||||
GLuint defaultFramebuffer;
|
||||
|
||||
std::unique_ptr<OpenGL::TextureTarget> compositeTexture; // receives raw composite levels
|
||||
std::unique_ptr<OpenGL::TextureTarget> filteredYTexture; // receives filtered Y in the R channel plus unfiltered I/U and Q/V in G and B
|
||||
std::unique_ptr<OpenGL::TextureTarget> filteredTexture; // receives filtered YIQ or YUV
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
using namespace Outputs::CRT;
|
||||
|
||||
namespace {
|
||||
static const GLenum first_supplied_buffer_texture_unit = 3;
|
||||
}
|
||||
|
||||
OpenGLOutputBuilder::OpenGLOutputBuilder(unsigned int number_of_buffers, va_list sizes) :
|
||||
_run_write_pointer(0),
|
||||
_output_mutex(new std::mutex),
|
||||
_visible_area(Rect(0, 0, 1, 1)),
|
||||
_composite_src_output_y(0),
|
||||
_composite_shader(nullptr),
|
||||
_rgb_shader(nullptr)
|
||||
{
|
||||
_run_builders = new CRTRunBuilder *[NumberOfFields];
|
||||
for(int builder = 0; builder < NumberOfFields; builder++)
|
||||
{
|
||||
_run_builders[builder] = new CRTRunBuilder(OutputVertexSize);
|
||||
}
|
||||
_composite_src_runs = std::unique_ptr<CRTRunBuilder>(new CRTRunBuilder(InputVertexSize));
|
||||
|
||||
va_list va;
|
||||
va_copy(va, sizes);
|
||||
_buffer_builder = std::unique_ptr<CRTInputBufferBuilder>(new CRTInputBufferBuilder(number_of_buffers, sizes));
|
||||
va_end(va);
|
||||
}
|
||||
|
||||
OpenGLOutputBuilder::~OpenGLOutputBuilder()
|
||||
{
|
||||
for(int builder = 0; builder < NumberOfFields; builder++)
|
||||
{
|
||||
delete _run_builders[builder];
|
||||
}
|
||||
delete[] _run_builders;
|
||||
|
||||
free(_composite_shader);
|
||||
free(_rgb_shader);
|
||||
}
|
||||
|
||||
static GLenum formatForDepth(size_t depth)
|
||||
{
|
||||
switch(depth)
|
||||
@ -57,33 +62,17 @@ static GLenum formatForDepth(size_t depth)
|
||||
}
|
||||
}
|
||||
|
||||
void CRT::construct_openGL()
|
||||
{
|
||||
_openGL_state = nullptr;
|
||||
_composite_shader = _rgb_shader = nullptr;
|
||||
}
|
||||
|
||||
void CRT::destruct_openGL()
|
||||
{
|
||||
delete _openGL_state;
|
||||
_openGL_state = nullptr;
|
||||
if(_composite_shader) free(_composite_shader);
|
||||
if(_rgb_shader) free(_rgb_shader);
|
||||
}
|
||||
|
||||
void CRT::draw_frame(unsigned int output_width, unsigned int output_height, bool only_if_dirty)
|
||||
void OpenGLOutputBuilder::draw_frame(unsigned int output_width, unsigned int output_height, bool only_if_dirty)
|
||||
{
|
||||
// establish essentials
|
||||
if(!_openGL_state)
|
||||
if(!composite_input_shader_program && !rgb_shader_program)
|
||||
{
|
||||
_openGL_state = new OpenGLState;
|
||||
|
||||
// generate and bind textures for every one of the requested buffers
|
||||
for(unsigned int buffer = 0; buffer < _buffer_builder->number_of_buffers; buffer++)
|
||||
{
|
||||
glGenTextures(1, &_openGL_state->textureName);
|
||||
glGenTextures(1, &textureName);
|
||||
glActiveTexture(GL_TEXTURE0 + first_supplied_buffer_texture_unit + buffer);
|
||||
glBindTexture(GL_TEXTURE_2D, _openGL_state->textureName);
|
||||
glBindTexture(GL_TEXTURE_2D, textureName);
|
||||
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);
|
||||
@ -93,29 +82,29 @@ void CRT::draw_frame(unsigned int output_width, unsigned int output_height, bool
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, (GLint)format, InputBufferBuilderWidth, InputBufferBuilderHeight, 0, format, GL_UNSIGNED_BYTE, _buffer_builder->buffers[buffer].data);
|
||||
}
|
||||
|
||||
glGenVertexArrays(1, &_openGL_state->output_vertex_array);
|
||||
glGenBuffers(1, &_openGL_state->output_array_buffer);
|
||||
_openGL_state->output_vertices_per_slice = 0;
|
||||
glGenVertexArrays(1, &output_vertex_array);
|
||||
glGenBuffers(1, &output_array_buffer);
|
||||
output_vertices_per_slice = 0;
|
||||
|
||||
prepare_composite_input_shader();
|
||||
prepare_rgb_output_shader();
|
||||
|
||||
glBindBuffer(GL_ARRAY_BUFFER, _openGL_state->output_array_buffer);
|
||||
glBindVertexArray(_openGL_state->output_vertex_array);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, output_array_buffer);
|
||||
glBindVertexArray(output_vertex_array);
|
||||
prepare_output_vertex_array();
|
||||
|
||||
// This should return either an actual framebuffer number, if this is a target with a framebuffer intended for output,
|
||||
// or 0 if no framebuffer is bound, in which case 0 is also what we want to supply to bind the implied framebuffer. So
|
||||
// it works either way.
|
||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint *)&_openGL_state->defaultFramebuffer);
|
||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint *)&defaultFramebuffer);
|
||||
|
||||
// Create intermediate textures and bind to slots 0, 1 and 2
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
_openGL_state->compositeTexture = std::unique_ptr<OpenGL::TextureTarget>(new OpenGL::TextureTarget(IntermediateBufferWidth, IntermediateBufferHeight));
|
||||
compositeTexture = std::unique_ptr<OpenGL::TextureTarget>(new OpenGL::TextureTarget(IntermediateBufferWidth, IntermediateBufferHeight));
|
||||
glActiveTexture(GL_TEXTURE1);
|
||||
_openGL_state->filteredYTexture = std::unique_ptr<OpenGL::TextureTarget>(new OpenGL::TextureTarget(IntermediateBufferWidth, IntermediateBufferHeight));
|
||||
filteredYTexture = std::unique_ptr<OpenGL::TextureTarget>(new OpenGL::TextureTarget(IntermediateBufferWidth, IntermediateBufferHeight));
|
||||
glActiveTexture(GL_TEXTURE2);
|
||||
_openGL_state->filteredTexture = std::unique_ptr<OpenGL::TextureTarget>(new OpenGL::TextureTarget(IntermediateBufferWidth, IntermediateBufferHeight));
|
||||
filteredTexture = std::unique_ptr<OpenGL::TextureTarget>(new OpenGL::TextureTarget(IntermediateBufferWidth, IntermediateBufferHeight));
|
||||
}
|
||||
|
||||
// glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint *)&_openGL_state->defaultFramebuffer);
|
||||
@ -155,7 +144,7 @@ void CRT::draw_frame(unsigned int output_width, unsigned int output_height, bool
|
||||
// check for anything to decode from composite
|
||||
if(_composite_src_runs->number_of_vertices)
|
||||
{
|
||||
_openGL_state->composite_input_shader_program->bind();
|
||||
composite_input_shader_program->bind();
|
||||
_composite_src_runs->reset();
|
||||
}
|
||||
|
||||
@ -167,35 +156,35 @@ void CRT::draw_frame(unsigned int output_width, unsigned int output_height, bool
|
||||
// glGetIntegerv(GL_VIEWPORT, results);
|
||||
|
||||
// ensure array buffer is up to date
|
||||
glBindBuffer(GL_ARRAY_BUFFER, _openGL_state->output_array_buffer);
|
||||
glBindBuffer(GL_ARRAY_BUFFER, output_array_buffer);
|
||||
size_t max_number_of_vertices = 0;
|
||||
for(int c = 0; c < NumberOfFields; c++)
|
||||
{
|
||||
max_number_of_vertices = std::max(max_number_of_vertices, _run_builders[c]->number_of_vertices);
|
||||
}
|
||||
if(_openGL_state->output_vertices_per_slice < max_number_of_vertices)
|
||||
if(output_vertices_per_slice < max_number_of_vertices)
|
||||
{
|
||||
_openGL_state->output_vertices_per_slice = max_number_of_vertices;
|
||||
output_vertices_per_slice = max_number_of_vertices;
|
||||
glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)(max_number_of_vertices * OutputVertexSize * OutputVertexSize), NULL, GL_STREAM_DRAW);
|
||||
|
||||
for(unsigned int c = 0; c < NumberOfFields; c++)
|
||||
{
|
||||
uint8_t *data = &_run_builders[c]->_runs[0];
|
||||
glBufferSubData(GL_ARRAY_BUFFER, (GLsizeiptr)(c * _openGL_state->output_vertices_per_slice * OutputVertexSize), (GLsizeiptr)(_run_builders[c]->number_of_vertices * OutputVertexSize), data);
|
||||
glBufferSubData(GL_ARRAY_BUFFER, (GLsizeiptr)(c * output_vertices_per_slice * OutputVertexSize), (GLsizeiptr)(_run_builders[c]->number_of_vertices * OutputVertexSize), data);
|
||||
_run_builders[c]->uploaded_vertices = _run_builders[c]->number_of_vertices;
|
||||
}
|
||||
}
|
||||
|
||||
// switch to the output shader
|
||||
if(_openGL_state->rgb_shader_program)
|
||||
if(rgb_shader_program)
|
||||
{
|
||||
_openGL_state->rgb_shader_program->bind();
|
||||
rgb_shader_program->bind();
|
||||
|
||||
// update uniforms
|
||||
push_size_uniforms(output_width, output_height);
|
||||
|
||||
// Ensure we're back on the output framebuffer
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, _openGL_state->defaultFramebuffer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebuffer);
|
||||
|
||||
// clear the buffer
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
@ -211,19 +200,19 @@ void CRT::draw_frame(unsigned int output_width, unsigned int output_height, bool
|
||||
|
||||
if(_run_builders[run]->number_of_vertices > 0)
|
||||
{
|
||||
glUniform1f(_openGL_state->timestampBaseUniform, (GLfloat)total_age);
|
||||
glUniform1f(timestampBaseUniform, (GLfloat)total_age);
|
||||
|
||||
if(_run_builders[run]->uploaded_vertices != _run_builders[run]->number_of_vertices)
|
||||
{
|
||||
uint8_t *data = &_run_builders[run]->_runs[_run_builders[run]->uploaded_vertices * OutputVertexSize];
|
||||
glBufferSubData(GL_ARRAY_BUFFER,
|
||||
(GLsizeiptr)(((run * _openGL_state->output_vertices_per_slice) + _run_builders[run]->uploaded_vertices) * OutputVertexSize),
|
||||
(GLsizeiptr)(((run * output_vertices_per_slice) + _run_builders[run]->uploaded_vertices) * OutputVertexSize),
|
||||
(GLsizeiptr)((_run_builders[run]->number_of_vertices - _run_builders[run]->uploaded_vertices) * OutputVertexSize), data);
|
||||
_run_builders[run]->uploaded_vertices = _run_builders[run]->number_of_vertices;
|
||||
}
|
||||
|
||||
// draw this frame
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, (GLint)(run * _openGL_state->output_vertices_per_slice), (GLsizei)_run_builders[run]->number_of_vertices);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, (GLint)(run * output_vertices_per_slice), (GLsizei)_run_builders[run]->number_of_vertices);
|
||||
}
|
||||
|
||||
// advance back in time
|
||||
@ -234,16 +223,15 @@ void CRT::draw_frame(unsigned int output_width, unsigned int output_height, bool
|
||||
_output_mutex->unlock();
|
||||
}
|
||||
|
||||
void CRT::set_openGL_context_will_change(bool should_delete_resources)
|
||||
void OpenGLOutputBuilder::set_openGL_context_will_change(bool should_delete_resources)
|
||||
{
|
||||
_openGL_state = nullptr;
|
||||
}
|
||||
|
||||
void CRT::push_size_uniforms(unsigned int output_width, unsigned int output_height)
|
||||
void OpenGLOutputBuilder::push_size_uniforms(unsigned int output_width, unsigned int output_height)
|
||||
{
|
||||
if(_openGL_state->windowSizeUniform >= 0)
|
||||
if(windowSizeUniform >= 0)
|
||||
{
|
||||
glUniform2f(_openGL_state->windowSizeUniform, output_width, output_height);
|
||||
glUniform2f(windowSizeUniform, output_width, output_height);
|
||||
}
|
||||
|
||||
GLfloat outputAspectRatioMultiplier = ((float)output_width / (float)output_height) / (4.0f / 3.0f);
|
||||
@ -254,26 +242,26 @@ void CRT::push_size_uniforms(unsigned int output_width, unsigned int output_heig
|
||||
_aspect_ratio_corrected_bounds.origin.x -= bonusWidth * 0.5f * _aspect_ratio_corrected_bounds.size.width;
|
||||
_aspect_ratio_corrected_bounds.size.width *= outputAspectRatioMultiplier;
|
||||
|
||||
if(_openGL_state->boundsOriginUniform >= 0)
|
||||
glUniform2f(_openGL_state->boundsOriginUniform, (GLfloat)_aspect_ratio_corrected_bounds.origin.x, (GLfloat)_aspect_ratio_corrected_bounds.origin.y);
|
||||
if(boundsOriginUniform >= 0)
|
||||
glUniform2f(boundsOriginUniform, (GLfloat)_aspect_ratio_corrected_bounds.origin.x, (GLfloat)_aspect_ratio_corrected_bounds.origin.y);
|
||||
|
||||
if(_openGL_state->boundsSizeUniform >= 0)
|
||||
glUniform2f(_openGL_state->boundsSizeUniform, (GLfloat)_aspect_ratio_corrected_bounds.size.width, (GLfloat)_aspect_ratio_corrected_bounds.size.height);
|
||||
if(boundsSizeUniform >= 0)
|
||||
glUniform2f(boundsSizeUniform, (GLfloat)_aspect_ratio_corrected_bounds.size.width, (GLfloat)_aspect_ratio_corrected_bounds.size.height);
|
||||
}
|
||||
|
||||
void CRT::set_composite_sampling_function(const char *shader)
|
||||
void OpenGLOutputBuilder::set_composite_sampling_function(const char *shader)
|
||||
{
|
||||
_composite_shader = strdup(shader);
|
||||
}
|
||||
|
||||
void CRT::set_rgb_sampling_function(const char *shader)
|
||||
void OpenGLOutputBuilder::set_rgb_sampling_function(const char *shader)
|
||||
{
|
||||
_rgb_shader = strdup(shader);
|
||||
}
|
||||
|
||||
#pragma mark - Input vertex shader (i.e. from source data to intermediate line layout)
|
||||
|
||||
char *CRT::get_input_vertex_shader()
|
||||
char *OpenGLOutputBuilder::get_input_vertex_shader()
|
||||
{
|
||||
return strdup(
|
||||
"#version 150\n"
|
||||
@ -298,7 +286,7 @@ char *CRT::get_input_vertex_shader()
|
||||
"}");
|
||||
}
|
||||
|
||||
char *CRT::get_input_fragment_shader()
|
||||
char *OpenGLOutputBuilder::get_input_fragment_shader()
|
||||
{
|
||||
const char *composite_shader = _composite_shader;
|
||||
if(!composite_shader)
|
||||
@ -329,7 +317,7 @@ char *CRT::get_input_fragment_shader()
|
||||
|
||||
#pragma mark - Output vertex shader
|
||||
|
||||
char *CRT::get_output_vertex_shader()
|
||||
char *OpenGLOutputBuilder::get_output_vertex_shader()
|
||||
{
|
||||
// the main job of the vertex shader is just to map from an input area of [0,1]x[0,1], with the origin in the
|
||||
// top left to OpenGL's [-1,1]x[-1,1] with the origin in the lower left, and to convert input data coordinates
|
||||
@ -378,12 +366,12 @@ char *CRT::get_output_vertex_shader()
|
||||
|
||||
#pragma mark - Output fragment shaders; RGB and from composite
|
||||
|
||||
char *CRT::get_rgb_output_fragment_shader()
|
||||
char *OpenGLOutputBuilder::get_rgb_output_fragment_shader()
|
||||
{
|
||||
return get_output_fragment_shader(_rgb_shader);
|
||||
}
|
||||
|
||||
char *CRT::get_composite_output_fragment_shader()
|
||||
char *OpenGLOutputBuilder::get_composite_output_fragment_shader()
|
||||
{
|
||||
return get_output_fragment_shader(
|
||||
"vec4 rgb_sample(vec2 coordinate)"
|
||||
@ -392,7 +380,7 @@ char *CRT::get_composite_output_fragment_shader()
|
||||
"}");
|
||||
}
|
||||
|
||||
char *CRT::get_output_fragment_shader(const char *sampling_function)
|
||||
char *OpenGLOutputBuilder::get_output_fragment_shader(const char *sampling_function)
|
||||
{
|
||||
return get_compound_shader(
|
||||
"#version 150\n"
|
||||
@ -418,7 +406,7 @@ char *CRT::get_output_fragment_shader(const char *sampling_function)
|
||||
|
||||
#pragma mark - Shader utilities
|
||||
|
||||
char *CRT::get_compound_shader(const char *base, const char *insert)
|
||||
char *OpenGLOutputBuilder::get_compound_shader(const char *base, const char *insert)
|
||||
{
|
||||
if(!base || !insert) return nullptr;
|
||||
size_t totalLength = strlen(base) + strlen(insert) + 1;
|
||||
@ -429,18 +417,18 @@ char *CRT::get_compound_shader(const char *base, const char *insert)
|
||||
|
||||
#pragma mark - Program compilation
|
||||
|
||||
void CRT::prepare_composite_input_shader()
|
||||
void OpenGLOutputBuilder::prepare_composite_input_shader()
|
||||
{
|
||||
char *vertex_shader = get_input_vertex_shader();
|
||||
char *fragment_shader = get_input_fragment_shader();
|
||||
if(vertex_shader && fragment_shader)
|
||||
{
|
||||
_openGL_state->composite_input_shader_program = std::unique_ptr<OpenGL::Shader>(new OpenGL::Shader(vertex_shader, fragment_shader));
|
||||
composite_input_shader_program = std::unique_ptr<OpenGL::Shader>(new OpenGL::Shader(vertex_shader, fragment_shader));
|
||||
|
||||
GLint texIDUniform = _openGL_state->composite_input_shader_program->get_uniform_location("texID");
|
||||
GLint inputTextureSizeUniform = _openGL_state->composite_input_shader_program->get_uniform_location("inputTextureSize");
|
||||
GLint outputTextureSizeUniform = _openGL_state->composite_input_shader_program->get_uniform_location("outputTextureSize");
|
||||
GLint phaseCyclesPerTickUniform = _openGL_state->composite_input_shader_program->get_uniform_location("phaseCyclesPerTick");
|
||||
GLint texIDUniform = composite_input_shader_program->get_uniform_location("texID");
|
||||
GLint inputTextureSizeUniform = composite_input_shader_program->get_uniform_location("inputTextureSize");
|
||||
GLint outputTextureSizeUniform = composite_input_shader_program->get_uniform_location("outputTextureSize");
|
||||
GLint phaseCyclesPerTickUniform = composite_input_shader_program->get_uniform_location("phaseCyclesPerTick");
|
||||
|
||||
glUniform1i(texIDUniform, first_supplied_buffer_texture_unit);
|
||||
glUniform2f(outputTextureSizeUniform, IntermediateBufferWidth, IntermediateBufferHeight);
|
||||
@ -451,7 +439,7 @@ void CRT::prepare_composite_input_shader()
|
||||
free(fragment_shader);
|
||||
}
|
||||
|
||||
/*void CRT::prepare_output_shader(char *fragment_shader)
|
||||
/*void OpenGLOutputBuilder::prepare_output_shader(char *fragment_shader)
|
||||
{
|
||||
char *vertex_shader = get_output_vertex_shader();
|
||||
if(vertex_shader && fragment_shader)
|
||||
@ -490,38 +478,38 @@ void CRT::prepare_composite_input_shader()
|
||||
free(fragment_shader);
|
||||
}*/
|
||||
|
||||
void CRT::prepare_rgb_output_shader()
|
||||
void OpenGLOutputBuilder::prepare_rgb_output_shader()
|
||||
{
|
||||
char *vertex_shader = get_output_vertex_shader();
|
||||
char *fragment_shader = get_rgb_output_fragment_shader();
|
||||
|
||||
if(vertex_shader && fragment_shader)
|
||||
{
|
||||
_openGL_state->rgb_shader_program = std::unique_ptr<OpenGL::Shader>(new OpenGL::Shader(vertex_shader, fragment_shader));
|
||||
rgb_shader_program = std::unique_ptr<OpenGL::Shader>(new OpenGL::Shader(vertex_shader, fragment_shader));
|
||||
|
||||
_openGL_state->rgb_shader_program->bind();
|
||||
rgb_shader_program->bind();
|
||||
|
||||
_openGL_state->windowSizeUniform = _openGL_state->rgb_shader_program->get_uniform_location("windowSize");
|
||||
_openGL_state->boundsSizeUniform = _openGL_state->rgb_shader_program->get_uniform_location("boundsSize");
|
||||
_openGL_state->boundsOriginUniform = _openGL_state->rgb_shader_program->get_uniform_location("boundsOrigin");
|
||||
_openGL_state->timestampBaseUniform = _openGL_state->rgb_shader_program->get_uniform_location("timestampBase");
|
||||
windowSizeUniform = rgb_shader_program->get_uniform_location("windowSize");
|
||||
boundsSizeUniform = rgb_shader_program->get_uniform_location("boundsSize");
|
||||
boundsOriginUniform = rgb_shader_program->get_uniform_location("boundsOrigin");
|
||||
timestampBaseUniform = rgb_shader_program->get_uniform_location("timestampBase");
|
||||
|
||||
GLint texIDUniform = _openGL_state->rgb_shader_program->get_uniform_location("texID");
|
||||
GLint shadowMaskTexIDUniform = _openGL_state->rgb_shader_program->get_uniform_location("shadowMaskTexID");
|
||||
GLint textureSizeUniform = _openGL_state->rgb_shader_program->get_uniform_location("textureSize");
|
||||
GLint ticksPerFrameUniform = _openGL_state->rgb_shader_program->get_uniform_location("ticksPerFrame");
|
||||
GLint scanNormalUniform = _openGL_state->rgb_shader_program->get_uniform_location("scanNormal");
|
||||
GLint positionConversionUniform = _openGL_state->rgb_shader_program->get_uniform_location("positionConversion");
|
||||
GLint texIDUniform = rgb_shader_program->get_uniform_location("texID");
|
||||
GLint shadowMaskTexIDUniform = rgb_shader_program->get_uniform_location("shadowMaskTexID");
|
||||
GLint textureSizeUniform = rgb_shader_program->get_uniform_location("textureSize");
|
||||
GLint ticksPerFrameUniform = rgb_shader_program->get_uniform_location("ticksPerFrame");
|
||||
GLint scanNormalUniform = rgb_shader_program->get_uniform_location("scanNormal");
|
||||
GLint positionConversionUniform = rgb_shader_program->get_uniform_location("positionConversion");
|
||||
|
||||
glUniform1i(texIDUniform, first_supplied_buffer_texture_unit);
|
||||
glUniform1i(shadowMaskTexIDUniform, 1);
|
||||
glUniform2f(textureSizeUniform, InputBufferBuilderWidth, InputBufferBuilderHeight);
|
||||
glUniform1f(ticksPerFrameUniform, (GLfloat)(_cycles_per_line * _height_of_display));
|
||||
glUniform2f(positionConversionUniform, _horizontal_flywheel->get_scan_period(), _vertical_flywheel->get_scan_period() / (unsigned int)_vertical_flywheel_output_divider);
|
||||
glUniform2f(positionConversionUniform, _horizontal_scan_period, _vertical_scan_period / (unsigned int)_vertical_period_divider);
|
||||
|
||||
float scan_angle = atan2f(1.0f / (float)_height_of_display, 1.0f);
|
||||
float scan_normal[] = { -sinf(scan_angle), cosf(scan_angle)};
|
||||
float multiplier = (float)_horizontal_flywheel->get_standard_period() / ((float)_height_of_display * (float)_horizontal_flywheel->get_scan_period());
|
||||
float multiplier = (float)_cycles_per_line / ((float)_height_of_display * (float)_horizontal_scan_period);
|
||||
scan_normal[0] *= multiplier;
|
||||
scan_normal[1] *= multiplier;
|
||||
glUniform2f(scanNormalUniform, scan_normal[0], scan_normal[1]);
|
||||
@ -531,14 +519,14 @@ void CRT::prepare_rgb_output_shader()
|
||||
free(fragment_shader);
|
||||
}
|
||||
|
||||
void CRT::prepare_output_vertex_array()
|
||||
void OpenGLOutputBuilder::prepare_output_vertex_array()
|
||||
{
|
||||
if(_openGL_state->rgb_shader_program)
|
||||
if(rgb_shader_program)
|
||||
{
|
||||
GLint positionAttribute = _openGL_state->rgb_shader_program->get_attrib_location("position");
|
||||
GLint textureCoordinatesAttribute = _openGL_state->rgb_shader_program->get_attrib_location("srcCoordinates");
|
||||
GLint lateralAttribute = _openGL_state->rgb_shader_program->get_attrib_location("lateral");
|
||||
GLint timestampAttribute = _openGL_state->rgb_shader_program->get_attrib_location("timestamp");
|
||||
GLint positionAttribute = rgb_shader_program->get_attrib_location("position");
|
||||
GLint textureCoordinatesAttribute = rgb_shader_program->get_attrib_location("srcCoordinates");
|
||||
GLint lateralAttribute = rgb_shader_program->get_attrib_location("lateral");
|
||||
GLint timestampAttribute = rgb_shader_program->get_attrib_location("timestamp");
|
||||
|
||||
glEnableVertexAttribArray((GLuint)positionAttribute);
|
||||
glEnableVertexAttribArray((GLuint)textureCoordinatesAttribute);
|
||||
@ -555,7 +543,7 @@ void CRT::prepare_output_vertex_array()
|
||||
|
||||
#pragma mark - Configuration
|
||||
|
||||
void CRT::set_output_device(OutputDevice output_device)
|
||||
void OpenGLOutputBuilder::set_output_device(OutputDevice output_device)
|
||||
{
|
||||
if (_output_device != output_device)
|
||||
{
|
||||
|
@ -9,6 +9,15 @@
|
||||
#ifndef CRTOpenGL_h
|
||||
#define CRTOpenGL_h
|
||||
|
||||
#include "../CRTTypes.hpp"
|
||||
#include "OpenGL.hpp"
|
||||
#include "TextureTarget.hpp"
|
||||
#include "Shader.hpp"
|
||||
#include "CRTInputBufferBuilder.hpp"
|
||||
#include "CRTRunBuilder.hpp"
|
||||
|
||||
#include <mutex>
|
||||
|
||||
namespace Outputs {
|
||||
namespace CRT {
|
||||
|
||||
@ -43,6 +52,184 @@ const int IntermediateBufferHeight = 2048;
|
||||
// number of historic fields that are required fully to
|
||||
const int NumberOfFields = 3;
|
||||
|
||||
class OpenGLOutputBuilder {
|
||||
private:
|
||||
// colour information
|
||||
ColourSpace _colour_space;
|
||||
unsigned int _colour_cycle_numerator;
|
||||
unsigned int _colour_cycle_denominator;
|
||||
OutputDevice _output_device;
|
||||
|
||||
// timing information to allow reasoning about input information
|
||||
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.
|
||||
char *_composite_shader;
|
||||
char *_rgb_shader;
|
||||
|
||||
// Methods used by the OpenGL code
|
||||
void prepare_rgb_output_shader();
|
||||
void prepare_composite_input_shader();
|
||||
void prepare_output_vertex_array();
|
||||
void push_size_uniforms(unsigned int output_width, unsigned int output_height);
|
||||
|
||||
// the run and input data buffers
|
||||
std::unique_ptr<CRTInputBufferBuilder> _buffer_builder;
|
||||
CRTRunBuilder **_run_builders;
|
||||
int _run_write_pointer;
|
||||
std::shared_ptr<std::mutex> _output_mutex;
|
||||
|
||||
// transient buffers indicating composite data not yet decoded
|
||||
std::unique_ptr<CRTRunBuilder> _composite_src_runs;
|
||||
uint16_t _composite_src_output_y;
|
||||
|
||||
char *get_output_vertex_shader();
|
||||
|
||||
char *get_output_fragment_shader(const char *sampling_function);
|
||||
char *get_rgb_output_fragment_shader();
|
||||
char *get_composite_output_fragment_shader();
|
||||
|
||||
char *get_input_vertex_shader();
|
||||
char *get_input_fragment_shader();
|
||||
|
||||
char *get_compound_shader(const char *base, const char *insert);
|
||||
|
||||
std::unique_ptr<OpenGL::Shader> rgb_shader_program;
|
||||
std::unique_ptr<OpenGL::Shader> composite_input_shader_program, composite_output_shader_program;
|
||||
|
||||
GLuint output_array_buffer, output_vertex_array;
|
||||
size_t output_vertices_per_slice;
|
||||
|
||||
GLint windowSizeUniform, timestampBaseUniform;
|
||||
GLint boundsOriginUniform, boundsSizeUniform;
|
||||
|
||||
GLuint textureName, shadowMaskTextureName;
|
||||
|
||||
GLuint defaultFramebuffer;
|
||||
|
||||
std::unique_ptr<OpenGL::TextureTarget> compositeTexture; // receives raw composite levels
|
||||
std::unique_ptr<OpenGL::TextureTarget> filteredYTexture; // receives filtered Y in the R channel plus unfiltered I/U and Q/V in G and B
|
||||
std::unique_ptr<OpenGL::TextureTarget> filteredTexture; // receives filtered YIQ or YUV
|
||||
|
||||
public:
|
||||
OpenGLOutputBuilder(unsigned int number_of_buffers, va_list sizes);
|
||||
~OpenGLOutputBuilder();
|
||||
|
||||
inline void set_colour_format(ColourSpace colour_space, unsigned int colour_cycle_numerator, unsigned int colour_cycle_denominator)
|
||||
{
|
||||
_colour_space = colour_space;
|
||||
_colour_cycle_numerator = colour_cycle_numerator;
|
||||
_colour_cycle_denominator = colour_cycle_denominator;
|
||||
}
|
||||
|
||||
inline void set_visible_area(Rect visible_area)
|
||||
{
|
||||
_visible_area = visible_area;
|
||||
}
|
||||
|
||||
inline uint8_t *get_next_input_run()
|
||||
{
|
||||
_output_mutex->lock();
|
||||
return (_output_device == Monitor) ? _run_builders[_run_write_pointer]->get_next_run(6) : _composite_src_runs->get_next_run(2);
|
||||
}
|
||||
|
||||
inline void complete_input_run()
|
||||
{
|
||||
_output_mutex->unlock();
|
||||
}
|
||||
|
||||
inline uint8_t *get_next_output_run()
|
||||
{
|
||||
_output_mutex->lock();
|
||||
return (_output_device == Monitor) ? _run_builders[_run_write_pointer]->get_next_run(6) : _composite_src_runs->get_next_run(2);
|
||||
}
|
||||
|
||||
inline void complete_output_run()
|
||||
{
|
||||
}
|
||||
|
||||
inline OutputDevice get_output_device()
|
||||
{
|
||||
return _output_device;
|
||||
}
|
||||
|
||||
inline uint32_t get_current_field_time()
|
||||
{
|
||||
return _run_builders[_run_write_pointer]->duration;
|
||||
}
|
||||
|
||||
inline void add_to_field_time(uint32_t amount)
|
||||
{
|
||||
_run_builders[_run_write_pointer]->duration += amount;
|
||||
}
|
||||
|
||||
inline uint16_t get_composite_output_y()
|
||||
{
|
||||
return _composite_src_output_y;
|
||||
}
|
||||
|
||||
inline void increment_composite_output_y()
|
||||
{
|
||||
_composite_src_output_y = (_composite_src_output_y + 1) % IntermediateBufferHeight;
|
||||
}
|
||||
|
||||
inline void increment_field()
|
||||
{
|
||||
_run_write_pointer = (_run_write_pointer + 1)%NumberOfFields;
|
||||
_run_builders[_run_write_pointer]->reset();
|
||||
}
|
||||
|
||||
inline void allocate_write_area(size_t required_length)
|
||||
{
|
||||
_output_mutex->lock();
|
||||
_buffer_builder->allocate_write_area(required_length);
|
||||
_output_mutex->unlock();
|
||||
}
|
||||
|
||||
inline void reduce_previous_allocation_to(size_t actual_length)
|
||||
{
|
||||
_buffer_builder->reduce_previous_allocation_to(actual_length);
|
||||
}
|
||||
|
||||
inline uint8_t *get_write_target_for_buffer(int buffer)
|
||||
{
|
||||
return _buffer_builder->get_write_target_for_buffer(buffer);
|
||||
}
|
||||
|
||||
inline uint16_t get_last_write_x_posiiton()
|
||||
{
|
||||
return _buffer_builder->_write_x_position;
|
||||
}
|
||||
|
||||
inline uint16_t get_last_write_y_posiiton()
|
||||
{
|
||||
return _buffer_builder->_write_y_position;
|
||||
}
|
||||
|
||||
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 char *shader);
|
||||
void set_rgb_sampling_function(const char *shader);
|
||||
void set_output_device(OutputDevice output_device);
|
||||
inline void set_timing(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)
|
||||
{
|
||||
_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;
|
||||
|
||||
// TODO: update related uniforms
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,8 @@
|
||||
#ifndef CRTRunBuilder_h
|
||||
#define CRTRunBuilder_h
|
||||
|
||||
#import <vector>
|
||||
|
||||
namespace Outputs {
|
||||
namespace CRT {
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user