1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-12-02 02:49:28 +00:00
CLK/Outputs/CRT/Internals/CRTOpenGL.cpp
2017-07-21 21:28:04 -04:00

419 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 <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),
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_);
}
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_ || (unsigned int)framebuffer_->get_height() != output_height || (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((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 std::string &shader) {
std::lock_guard<std::mutex> lock_guard(output_mutex_);
composite_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();
}
#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_.size()) {
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) {
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();
}
#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);
}
}