mirror of
https://github.com/TomHarte/CLK.git
synced 2024-08-16 16:28:59 +00:00
478 lines
16 KiB
C++
478 lines
16 KiB
C++
// CRTOpenGL.cpp
|
|
// Clock Signal
|
|
//
|
|
// Created by Thomas Harte on 03/02/2016.
|
|
// Copyright © 2016 Thomas Harte. All rights reserved.
|
|
//
|
|
|
|
#include "CRT.hpp"
|
|
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <cmath>
|
|
|
|
#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(size_t bytes_per_pixel) :
|
|
visible_area_(Rect(0, 0, 1, 1)),
|
|
composite_src_output_y_(0),
|
|
composite_shader_(nullptr),
|
|
rgb_shader_(nullptr),
|
|
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.6f, 0.6f, 0.6f, 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(!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_);
|
|
|
|
free(composite_shader_);
|
|
free(rgb_shader_);
|
|
}
|
|
|
|
bool OpenGLOutputBuilder::get_is_television_output()
|
|
{
|
|
return output_device_ == Television || !rgb_input_shader_program_;
|
|
}
|
|
|
|
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_rgb_input_shaders();
|
|
prepare_source_vertex_array();
|
|
|
|
prepare_output_shader();
|
|
prepare_output_vertex_array();
|
|
|
|
set_timing_uniforms();
|
|
set_colour_space_uniforms();
|
|
}
|
|
|
|
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 there's a target to draw to
|
|
if(!framebuffer_ || framebuffer_->get_height() != output_height || 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((float)output_width / (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.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
|
|
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}
|
|
};
|
|
|
|
// for RGB video, there's only two steps
|
|
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}
|
|
};
|
|
|
|
RenderStage *active_pipeline = get_is_television_output() ? composite_render_stages : rgb_render_stages;
|
|
|
|
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;
|
|
}
|
|
output_shader_program_->bind();
|
|
|
|
// draw
|
|
glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, (GLsizei)array_submission.output_size / OutputVertexSize);
|
|
}
|
|
|
|
#ifdef GL_NV_texture_barrier
|
|
glTextureBarrierNV();
|
|
#endif
|
|
|
|
// copy framebuffer to the intended place
|
|
glDisable(GL_BLEND);
|
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
|
glViewport(0, 0, (GLsizei)output_width, (GLsizei)output_height);
|
|
|
|
glActiveTexture(pixel_accumulation_texture_unit);
|
|
framebuffer_->bind_texture();
|
|
framebuffer_->draw((float)output_width / (float)output_height);
|
|
|
|
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;
|
|
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 char *shader)
|
|
{
|
|
std::lock_guard<std::mutex> lock_guard(output_mutex_);
|
|
composite_shader_ = strdup(shader);
|
|
reset_all_OpenGL_state();
|
|
}
|
|
|
|
void OpenGLOutputBuilder::set_rgb_sampling_function(const char *shader)
|
|
{
|
|
std::lock_guard<std::mutex> lock_guard(output_mutex_);
|
|
rgb_shader_ = strdup(shader);
|
|
reset_all_OpenGL_state();
|
|
}
|
|
|
|
#pragma mark - Program compilation
|
|
|
|
void OpenGLOutputBuilder::prepare_composite_input_shaders()
|
|
{
|
|
composite_input_shader_program_ = OpenGL::IntermediateShader::make_source_conversion_shader(composite_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);
|
|
|
|
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_rgb_input_shaders()
|
|
{
|
|
if(rgb_shader_)
|
|
{
|
|
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_)
|
|
{
|
|
glBindVertexArray(source_vertex_array_);
|
|
array_builder.bind_input();
|
|
|
|
composite_input_shader_program_->enable_vertex_attribute_with_pointer("inputStart", 2, GL_UNSIGNED_SHORT, GL_FALSE, SourceVertexSize, (void *)SourceVertexOffsetOfInputStart, 1);
|
|
composite_input_shader_program_->enable_vertex_attribute_with_pointer("outputStart", 2, GL_UNSIGNED_SHORT, GL_FALSE, SourceVertexSize, (void *)SourceVertexOffsetOfOutputStart, 1);
|
|
composite_input_shader_program_->enable_vertex_attribute_with_pointer("ends", 2, GL_UNSIGNED_SHORT, GL_FALSE, SourceVertexSize, (void *)SourceVertexOffsetOfEnds, 1);
|
|
composite_input_shader_program_->enable_vertex_attribute_with_pointer("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();
|
|
output_shader_program_->enable_vertex_attribute_with_pointer("horizontal", 2, GL_UNSIGNED_SHORT, GL_FALSE, OutputVertexSize, (void *)OutputVertexOffsetOfHorizontal, 1);
|
|
output_shader_program_->enable_vertex_attribute_with_pointer("vertical", 2, GL_UNSIGNED_SHORT, GL_FALSE, OutputVertexSize, (void *)OutputVertexOffsetOfVertical, 1);
|
|
}
|
|
}
|
|
|
|
#pragma mark - Public Configuration
|
|
|
|
void OpenGLOutputBuilder::set_output_device(OutputDevice output_device)
|
|
{
|
|
if(output_device_ != output_device)
|
|
{
|
|
output_device_ = output_device;
|
|
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)
|
|
{
|
|
output_mutex_.lock();
|
|
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();
|
|
output_mutex_.unlock();
|
|
}
|
|
|
|
#pragma 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, *toRGB;
|
|
|
|
switch(colour_space_)
|
|
{
|
|
case ColourSpace::YIQ:
|
|
fromRGB = rgbToYIQ;
|
|
toRGB = yiqToRGB;
|
|
break;
|
|
|
|
case ColourSpace::YUV:
|
|
fromRGB = rgbToYUV;
|
|
toRGB = yuvToRGB;
|
|
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);
|
|
}
|
|
|
|
float OpenGLOutputBuilder::get_composite_output_width() const
|
|
{
|
|
return ((float)colour_cycle_numerator_ * 4.0f) / (float)(colour_cycle_denominator_ * IntermediateBufferWidth);
|
|
}
|
|
|
|
void OpenGLOutputBuilder::set_output_shader_width()
|
|
{
|
|
if(output_shader_program_)
|
|
{
|
|
const float width = get_is_television_output() ? get_composite_output_width() : 1.0f;
|
|
output_shader_program_->set_input_width_scaler(width);
|
|
}
|
|
}
|
|
|
|
void OpenGLOutputBuilder::set_timing_uniforms()
|
|
{
|
|
const float colour_subcarrier_frequency = (float)colour_cycle_numerator_ / (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, (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(rgb_input_shader_program_)
|
|
{
|
|
rgb_input_shader_program_->set_width_scalers(1.0f, 1.0f);
|
|
}
|
|
}
|