mirror of
https://github.com/TomHarte/CLK.git
synced 2024-12-27 16:31:31 +00:00
471 lines
18 KiB
C++
471 lines
18 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 <cassert>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
|
|
#include "CRTOpenGL.hpp"
|
|
#include "../../../SignalProcessing/FIRFilter.hpp"
|
|
#include "Shaders/OutputShader.hpp"
|
|
|
|
using namespace Outputs::CRT;
|
|
|
|
namespace {
|
|
static const GLenum source_data_texture_unit = GL_TEXTURE0;
|
|
static const GLenum pixel_accumulation_texture_unit = GL_TEXTURE1;
|
|
|
|
static const GLenum composite_texture_unit = GL_TEXTURE2;
|
|
static const GLenum separated_texture_unit = GL_TEXTURE3;
|
|
static const GLenum filtered_texture_unit = GL_TEXTURE4;
|
|
|
|
static const GLenum work_texture_unit = GL_TEXTURE2;
|
|
}
|
|
|
|
OpenGLOutputBuilder::OpenGLOutputBuilder(std::size_t bytes_per_pixel) :
|
|
visible_area_(Rect(0, 0, 1, 1)),
|
|
composite_src_output_y_(0),
|
|
last_output_width_(0),
|
|
last_output_height_(0),
|
|
fence_(nullptr),
|
|
texture_builder(bytes_per_pixel, source_data_texture_unit),
|
|
array_builder(SourceVertexBufferDataSize, OutputVertexBufferDataSize) {
|
|
glBlendFunc(GL_SRC_ALPHA, GL_CONSTANT_COLOR);
|
|
glBlendColor(0.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(!std::strcmp(extension_name, "GL_NV_texture_barrier")) {
|
|
// supports_texture_barrier = true;
|
|
// }
|
|
// }
|
|
#endif
|
|
|
|
// if(supports_texture_barrier) {
|
|
// work_texture_.reset(new OpenGL::TextureTarget(IntermediateBufferWidth, IntermediateBufferHeight*2, work_texture_unit));
|
|
// } else {
|
|
composite_texture_.reset(new OpenGL::TextureTarget(IntermediateBufferWidth, IntermediateBufferHeight, composite_texture_unit, GL_NEAREST));
|
|
separated_texture_.reset(new OpenGL::TextureTarget(IntermediateBufferWidth, IntermediateBufferHeight, separated_texture_unit, GL_NEAREST));
|
|
filtered_texture_.reset(new OpenGL::TextureTarget(IntermediateBufferWidth, IntermediateBufferHeight, filtered_texture_unit, GL_LINEAR));
|
|
// }
|
|
}
|
|
|
|
OpenGLOutputBuilder::~OpenGLOutputBuilder() {
|
|
glDeleteVertexArrays(1, &output_vertex_array_);
|
|
}
|
|
|
|
bool OpenGLOutputBuilder::get_is_television_output() {
|
|
return output_device_ == OutputDevice::Television || !rgb_input_shader_program_;
|
|
}
|
|
|
|
void OpenGLOutputBuilder::set_target_framebuffer(GLint target_framebuffer) {
|
|
target_framebuffer_ = target_framebuffer;
|
|
}
|
|
|
|
void OpenGLOutputBuilder::draw_frame(unsigned int output_width, unsigned int output_height, bool only_if_dirty) {
|
|
// lock down any other draw_frames
|
|
draw_mutex_.lock();
|
|
|
|
// establish essentials
|
|
if(!output_shader_program_) {
|
|
prepare_composite_input_shaders();
|
|
prepare_rgb_input_shaders();
|
|
prepare_source_vertex_array();
|
|
|
|
prepare_output_shader();
|
|
prepare_output_vertex_array();
|
|
|
|
set_timing_uniforms();
|
|
set_colour_space_uniforms();
|
|
set_gamma();
|
|
}
|
|
|
|
if(fence_ != nullptr) {
|
|
// if the GPU is still busy, don't wait; we'll catch it next time
|
|
if(glClientWaitSync(fence_, GL_SYNC_FLUSH_COMMANDS_BIT, only_if_dirty ? 0 : GL_TIMEOUT_IGNORED) == GL_TIMEOUT_EXPIRED) {
|
|
draw_mutex_.unlock();
|
|
return;
|
|
}
|
|
|
|
glDeleteSync(fence_);
|
|
}
|
|
|
|
// make sure everything is bound
|
|
composite_texture_->bind_texture();
|
|
separated_texture_->bind_texture();
|
|
filtered_texture_->bind_texture();
|
|
if(work_texture_) work_texture_->bind_texture();
|
|
|
|
// make sure there's a target to draw to
|
|
if(!framebuffer_ || static_cast<unsigned int>(framebuffer_->get_height()) != output_height || static_cast<unsigned int>(framebuffer_->get_width()) != output_width) {
|
|
std::unique_ptr<OpenGL::TextureTarget> new_framebuffer(new OpenGL::TextureTarget((GLsizei)output_width, (GLsizei)output_height, pixel_accumulation_texture_unit, GL_LINEAR));
|
|
if(framebuffer_) {
|
|
new_framebuffer->bind_framebuffer();
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
|
|
glActiveTexture(pixel_accumulation_texture_unit);
|
|
framebuffer_->bind_texture();
|
|
framebuffer_->draw(static_cast<float>(output_width) / static_cast<float>(output_height));
|
|
|
|
new_framebuffer->bind_texture();
|
|
}
|
|
framebuffer_ = std::move(new_framebuffer);
|
|
}
|
|
|
|
// lock out the machine emulation until data is copied
|
|
output_mutex_.lock();
|
|
|
|
// release the mapping, giving up on trying to draw if data has been lost
|
|
ArrayBuilder::Submission array_submission = array_builder.submit();
|
|
|
|
// upload new source pixels, if any
|
|
glActiveTexture(source_data_texture_unit);
|
|
texture_builder.bind();
|
|
texture_builder.submit();
|
|
|
|
// buffer usage restart from 0 for the next time around
|
|
composite_src_output_y_ = 0;
|
|
|
|
// data having been grabbed, allow the machine to continue
|
|
output_mutex_.unlock();
|
|
|
|
struct RenderStage {
|
|
OpenGL::Shader *const shader;
|
|
OpenGL::TextureTarget *const target;
|
|
float clear_colour[3];
|
|
};
|
|
|
|
// for composite video, go through four steps to get to something that can be painted to the output
|
|
RenderStage composite_render_stages[] = {
|
|
{composite_input_shader_program_.get(), composite_texture_.get(), {0.0, 0.0, 0.0}},
|
|
{composite_separation_filter_program_.get(), separated_texture_.get(), {0.0, 0.5, 0.5}},
|
|
{composite_chrominance_filter_shader_program_.get(), filtered_texture_.get(), {0.0, 0.0, 0.0}},
|
|
{nullptr, nullptr}
|
|
};
|
|
|
|
// for 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, 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, static_cast<GLuint>(target_framebuffer_));
|
|
glViewport(0, 0, (GLsizei)output_width, (GLsizei)output_height);
|
|
|
|
glActiveTexture(pixel_accumulation_texture_unit);
|
|
framebuffer_->bind_texture();
|
|
framebuffer_->draw(static_cast<float>(output_width) / static_cast<float>(output_height));
|
|
|
|
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();
|
|
}
|
|
|
|
// 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();
|
|
|
|
using Shader = OpenGL::IntermediateShader;
|
|
composite_input_shader_program_->enable_vertex_attribute_with_pointer(
|
|
Shader::get_input_name(Shader::Input::InputStart),
|
|
2, GL_UNSIGNED_SHORT, GL_FALSE, SourceVertexSize,
|
|
(void *)SourceVertexOffsetOfInputStart, 1);
|
|
|
|
composite_input_shader_program_->enable_vertex_attribute_with_pointer(
|
|
Shader::get_input_name(Shader::Input::OutputStart),
|
|
2, GL_UNSIGNED_SHORT, GL_FALSE, SourceVertexSize,
|
|
(void *)SourceVertexOffsetOfOutputStart, 1);
|
|
|
|
composite_input_shader_program_->enable_vertex_attribute_with_pointer(
|
|
Shader::get_input_name(Shader::Input::Ends),
|
|
2, GL_UNSIGNED_SHORT, GL_FALSE, SourceVertexSize,
|
|
(void *)SourceVertexOffsetOfEnds, 1);
|
|
|
|
composite_input_shader_program_->enable_vertex_attribute_with_pointer(
|
|
Shader::get_input_name(Shader::Input::PhaseTimeAndAmplitude),
|
|
3, GL_UNSIGNED_BYTE, GL_FALSE, SourceVertexSize,
|
|
(void *)SourceVertexOffsetOfPhaseTimeAndAmplitude, 1);
|
|
}
|
|
}
|
|
|
|
void OpenGLOutputBuilder::prepare_output_shader() {
|
|
output_shader_program_ = OpenGL::OutputShader::make_shader("", "texture(texID, srcCoordinatesVarying).rgb", false);
|
|
output_shader_program_->set_source_texture_unit(work_texture_ ? work_texture_unit : filtered_texture_unit);
|
|
// output_shader_program_->set_source_texture_unit(composite_texture_unit);
|
|
output_shader_program_->set_origin_is_double_height(!!work_texture_);
|
|
}
|
|
|
|
void OpenGLOutputBuilder::prepare_output_vertex_array() {
|
|
if(output_shader_program_) {
|
|
glBindVertexArray(output_vertex_array_);
|
|
array_builder.bind_output();
|
|
|
|
using Shader = OpenGL::OutputShader;
|
|
output_shader_program_->enable_vertex_attribute_with_pointer(
|
|
Shader::get_input_name(Shader::Input::Horizontal),
|
|
2, GL_UNSIGNED_SHORT, GL_FALSE, OutputVertexSize,
|
|
(void *)OutputVertexOffsetOfHorizontal, 1);
|
|
|
|
output_shader_program_->enable_vertex_attribute_with_pointer(
|
|
Shader::get_input_name(Shader::Input::Vertical),
|
|
2, GL_UNSIGNED_SHORT, GL_FALSE, OutputVertexSize,
|
|
(void *)OutputVertexOffsetOfVertical, 1);
|
|
}
|
|
}
|
|
|
|
// MARK: - Public Configuration
|
|
|
|
void OpenGLOutputBuilder::set_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();
|
|
}
|
|
|
|
// 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;
|
|
|
|
default: assert(false); break;
|
|
}
|
|
|
|
if(composite_input_shader_program_) composite_input_shader_program_->set_colour_conversion_matrices(fromRGB, toRGB);
|
|
if(composite_separation_filter_program_) composite_separation_filter_program_->set_colour_conversion_matrices(fromRGB, toRGB);
|
|
if(composite_chrominance_filter_shader_program_) composite_chrominance_filter_shader_program_->set_colour_conversion_matrices(fromRGB, toRGB);
|
|
}
|
|
|
|
void OpenGLOutputBuilder::set_gamma() {
|
|
if(output_shader_program_) output_shader_program_->set_gamma_ratio(gamma_);
|
|
}
|
|
|
|
/*!
|
|
@returns The multiplier to apply to x positions received at the shader in order to produce locations in the intermediate
|
|
texture. Intermediate textures are in phase with the composite signal, so this is a function of (i) composite frequency
|
|
(determining how much of the texture adds up to a single line); and (ii) input frequency (determining what the input
|
|
positions mean as a fraction of a line).
|
|
*/
|
|
float OpenGLOutputBuilder::get_composite_output_width() const {
|
|
return
|
|
(static_cast<float>(colour_cycle_numerator_ * 4) / static_cast<float>(colour_cycle_denominator_ * IntermediateBufferWidth)) *
|
|
(static_cast<float>(IntermediateBufferWidth) / static_cast<float>(cycles_per_line_));
|
|
}
|
|
|
|
void OpenGLOutputBuilder::set_output_shader_width() {
|
|
if(output_shader_program_) {
|
|
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 = static_cast<float>(colour_cycle_numerator_) / static_cast<float>(colour_cycle_denominator_);
|
|
const float output_width = get_composite_output_width();
|
|
const float sample_cycles_per_line = cycles_per_line_ / output_width;
|
|
|
|
if(composite_separation_filter_program_) {
|
|
composite_separation_filter_program_->set_width_scalers(output_width, output_width);
|
|
composite_separation_filter_program_->set_separation_frequency(sample_cycles_per_line, colour_subcarrier_frequency);
|
|
composite_separation_filter_program_->set_extension(6.0f);
|
|
}
|
|
if(composite_chrominance_filter_shader_program_) {
|
|
composite_chrominance_filter_shader_program_->set_width_scalers(output_width, output_width);
|
|
composite_chrominance_filter_shader_program_->set_extension(5.0f);
|
|
}
|
|
if(rgb_filter_shader_program_) {
|
|
rgb_filter_shader_program_->set_width_scalers(1.0f, 1.0f);
|
|
rgb_filter_shader_program_->set_filter_coefficients(sample_cycles_per_line, static_cast<float>(input_frequency_) * 0.5f);
|
|
}
|
|
if(output_shader_program_) {
|
|
set_output_shader_width();
|
|
output_shader_program_->set_timing(height_of_display_, cycles_per_line_, horizontal_scan_period_, vertical_scan_period_, vertical_period_divider_);
|
|
}
|
|
if(composite_input_shader_program_) {
|
|
composite_input_shader_program_->set_width_scalers(1.0f, output_width);
|
|
composite_input_shader_program_->set_extension(0.0f);
|
|
}
|
|
if(rgb_input_shader_program_) {
|
|
rgb_input_shader_program_->set_width_scalers(1.0f, 1.0f);
|
|
}
|
|
}
|