1
0
mirror of https://github.com/TomHarte/CLK.git synced 2026-03-13 19:16:40 +00:00
Files
CLK/Outputs/OpenGL/ScanTarget.cpp
2026-02-21 23:19:18 -05:00

639 lines
19 KiB
C++

//
// ScanTarget.cpp
// Clock Signal
//
// Created by Thomas Harte on 05/11/2018.
// Copyright © 2018 Thomas Harte. All rights reserved.
//
#include "ScanTarget.hpp"
#include "OpenGL.hpp"
#include "Outputs/ScanTargets/FilterGenerator.hpp"
#include "Outputs/OpenGL/Shaders/CompositionShader.hpp"
#include "Outputs/OpenGL/Shaders/CopyShader.hpp"
#include "Outputs/OpenGL/Shaders/KernelShaders.hpp"
#include "Outputs/OpenGL/Shaders/LineOutputShader.hpp"
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace Outputs::Display::OpenGL;
namespace {
/// The texture unit from which to source input data.
constexpr GLenum SourceDataTextureUnit = GL_TEXTURE0;
/// Contains the initial composition of scans into lines.
constexpr GLenum CompositionTextureUnit = GL_TEXTURE1;
/// If the input data was composite, contains separated luma/chroma.
constexpr GLenum SeparationTextureUnit = GL_TEXTURE2;
/// If the input data was S-Video or composite, contains a fully demodulated image.
constexpr GLenum DemodulationTextureUnit = GL_TEXTURE3;
/// Contains the current display.
constexpr GLenum OutputTextureUnits[] = {GL_TEXTURE4, GL_TEXTURE5};
using Logger = Log::Logger<Log::Source::OpenGL>;
template <typename SourceT>
size_t submit(VertexArray &target, const size_t begin, const size_t end, const SourceT &source) {
if(begin == end) {
return 0;
}
target.bind_buffer();
size_t buffer_destination = 0;
const auto submit = [&](const size_t begin, const size_t end) {
test_gl([&]{
glBufferSubData(
GL_ARRAY_BUFFER,
buffer_destination,
(end - begin) * sizeof(source[0]),
&source[begin]
);
});
buffer_destination += (end - begin) * sizeof(source[0]);
};
if(begin < end) {
submit(begin, end);
return end - begin;
} else {
submit(begin, source.size());
submit(0, end);
return source.size() - begin + end;
}
}
size_t distance(const size_t begin, const size_t end, const size_t buffer_length) {
return end >= begin ? end - begin : buffer_length + end - begin;
}
}
ScanTarget::ScanTarget(const API api, const GLuint target_framebuffer, const float output_gamma) :
api_(api),
output_gamma_(output_gamma),
target_framebuffer_(target_framebuffer),
full_display_rectangle_(api, -1.0f, -1.0f, 2.0f, 2.0f),
scans_(scan_buffer_),
lines_(line_buffer_),
dirty_zones_(dirty_zones_buffer_) {
set_scan_buffer(scan_buffer_.data(), scan_buffer_.size());
set_line_buffer(line_buffer_.data(), line_buffer_.size());
// TODO: if this is OpenGL 4.4 or newer, use glBufferStorage rather than glBufferData
// and specify GL_MAP_PERSISTENT_BIT. Then map the buffer now, and let the client
// write straight into it.
// Set stencil function for underdraw.
test_gl([&]{ glStencilFunc(GL_EQUAL, 0, GLuint(~0)); });
test_gl([&]{ glStencilOp(GL_KEEP, GL_KEEP, GL_INCR); });
// Establish initial state for is_drawing_to_accumulation_buffer_.
is_drawing_to_output_.clear();
}
void ScanTarget::set_target_framebuffer(const GLuint target_framebuffer) {
perform([&] {
target_framebuffer_ = target_framebuffer;
});
}
void ScanTarget::update_aspect_ratio_transformation() {
if(output_buffers_[0].empty()) {
return;
}
const auto framing = aspect_ratio_transformation(
BufferingScanTarget::modals(),
float(output_buffers_[0].width()) / float(output_buffers_[0].height())
);
if(!line_output_shader_.empty()) {
line_output_shader_.set_aspect_ratio_transformation(framing);
}
if(!scan_output_shader_.empty()) {
scan_output_shader_.set_aspect_ratio_transformation(framing);
}
}
void ScanTarget::set_alphas() {
static constexpr float OutputAlpha = 0.64f;
if(!scan_output_shader_.empty()) {
scan_output_shader_.set_alpha(is_interlaced() ? std::sqrt(OutputAlpha) : OutputAlpha);
}
if(!line_output_shader_.empty()) {
line_output_shader_.set_alpha(is_interlaced() ? std::sqrt(OutputAlpha) : OutputAlpha);
}
}
void ScanTarget::setup_pipeline() {
const auto modals = BufferingScanTarget::modals();
const auto data_type_size = Outputs::Display::size_for_data_type(modals.input_data_type);
// Possibly create a new source texture.
if(source_texture_.empty() || source_texture_.channels() != data_type_size) {
source_texture_ = Texture(
data_type_size,
SourceDataTextureUnit,
GL_NEAREST,
GL_NEAREST,
WriteAreaWidth,
WriteAreaHeight
);
}
// Resize the texture if required.
const size_t required_size = WriteAreaWidth*WriteAreaHeight*data_type_size;
if(required_size != write_area_texture_.size()) {
write_area_texture_.resize(required_size);
set_write_area(write_area_texture_.data());
}
// Determine new sizing metrics.
const auto buffer_width = FilterGenerator::SuggestedBufferWidth;
const auto subcarrier_frequency = [](const Modals &modals) {
return float(modals.colour_cycle_numerator) / float(modals.colour_cycle_denominator);
};
const float sample_multiplier =
FilterGenerator::suggested_sample_multiplier(
subcarrier_frequency(modals),
modals.cycles_per_line
);
if(
copy_shader_.empty() ||
!existing_modals_ ||
existing_modals_->brightness != modals.brightness ||
existing_modals_->intended_gamma != modals.intended_gamma
) {
copy_shader_ = CopyShader(
api_,
modals.brightness != 1.0f ? std::optional<float>(modals.brightness) : std::optional<float>(),
modals.intended_gamma != output_gamma_ ?
std::optional<float>(output_gamma_ / modals.intended_gamma) :
std::optional<float>()
);
}
if(composition_buffer_.empty()) {
composition_buffer_ = TextureTarget(
api_,
buffer_width,
LineBufferHeight,
CompositionTextureUnit,
GL_NEAREST,
false
);
}
if(is_rgb(modals.display_type)) {
composition_shader_.reset();
separation_shader_.reset();
demodulation_shader_.reset();
line_output_shader_.reset();
if(
scan_output_shader_.empty() ||
existing_modals_->input_data_type != modals.input_data_type ||
existing_modals_->expected_vertical_lines != modals.expected_vertical_lines ||
existing_modals_->output_scale.x != modals.output_scale.x ||
existing_modals_->output_scale.y != modals.output_scale.y
) {
scan_output_shader_ = OpenGL::ScanOutputShader(
api_,
modals.input_data_type,
modals.expected_vertical_lines,
modals.output_scale.x,
modals.output_scale.y,
WriteAreaWidth,
WriteAreaHeight,
scans_,
SourceDataTextureUnit);
}
} else {
scan_output_shader_.reset();
if(
!existing_modals_ ||
existing_modals_->input_data_type != modals.input_data_type ||
existing_modals_->display_type != modals.display_type ||
existing_modals_->composite_colour_space != modals.composite_colour_space ||
subcarrier_frequency(*existing_modals_) != subcarrier_frequency(modals)
) {
composition_shader_ = OpenGL::composition_shader(
api_,
modals.input_data_type,
modals.display_type,
modals.composite_colour_space,
sample_multiplier,
WriteAreaWidth, WriteAreaHeight,
buffer_width, LineBufferHeight,
scans_,
GL_TEXTURE0
);
}
if(!is_composite(modals.display_type)) {
separation_shader_.reset();
} else if(
separation_shader_.empty() ||
modals.cycles_per_line != existing_modals_->cycles_per_line ||
subcarrier_frequency(*existing_modals_) != subcarrier_frequency(modals)
) {
separation_shader_ = OpenGL::separation_shader(
api_,
subcarrier_frequency(modals),
sample_multiplier * modals.cycles_per_line,
buffer_width, LineBufferHeight,
dirty_zones_,
CompositionTextureUnit
);
}
if(!is_composite(modals.display_type) && !is_svideo(modals.display_type)) {
demodulation_shader_.reset();
line_output_shader_.reset();
fill_shader_.reset();
} else {
if(
demodulation_shader_.empty() ||
!existing_modals_ ||
existing_modals_->display_type != modals.display_type ||
subcarrier_frequency(*existing_modals_) != subcarrier_frequency(modals)
) {
demodulation_shader_ = OpenGL::demodulation_shader(
api_,
modals.composite_colour_space,
modals.display_type,
subcarrier_frequency(modals),
sample_multiplier * modals.cycles_per_line,
buffer_width,
LineBufferHeight,
dirty_zones_,
is_svideo(modals.display_type) ? CompositionTextureUnit : SeparationTextureUnit
);
}
if(
line_output_shader_.empty() ||
!existing_modals_ ||
existing_modals_->display_type != modals.display_type ||
subcarrier_frequency(*existing_modals_) != subcarrier_frequency(modals)
) {
line_output_shader_ = LineOutputShader(
api_,
buffer_width, LineBufferHeight,
sample_multiplier,
modals.expected_vertical_lines,
modals.output_scale.x,
modals.output_scale.y,
lines_,
DemodulationTextureUnit
);
}
if(
fill_shader_.empty() ||
!existing_modals_ ||
existing_modals_->display_type != modals.display_type ||
subcarrier_frequency(*existing_modals_) != subcarrier_frequency(modals)
) {
fill_shader_ = OpenGL::FillShader(
api_,
sample_multiplier * modals.cycles_per_line,
buffer_width,
LineBufferHeight,
dirty_zones_
);
}
}
if(!is_composite(modals.display_type)) {
separation_buffer_.reset();
} else if(separation_buffer_.empty()) {
separation_buffer_ = TextureTarget(
api_,
buffer_width,
LineBufferHeight,
SeparationTextureUnit,
GL_NEAREST,
false
);
}
if(!is_composite(modals.display_type) && !is_svideo(modals.display_type)) {
demodulation_buffer_.reset();
} else if(demodulation_buffer_.empty()) {
demodulation_buffer_ = TextureTarget(
api_,
buffer_width,
LineBufferHeight,
DemodulationTextureUnit,
GL_LINEAR,
false
);
}
}
set_alphas();
update_aspect_ratio_transformation();
existing_modals_ = modals;
}
bool ScanTarget::is_soft_display_type() {
const auto display_type = modals().display_type;
return display_type == DisplayType::CompositeColour || display_type == DisplayType::CompositeMonochrome;
}
void ScanTarget::update(const int output_width, const int output_height) {
// If the GPU is still busy, don't wait; we'll catch it next time.
if(fence_ != nullptr) {
if(glClientWaitSync(fence_, GL_SYNC_FLUSH_COMMANDS_BIT, 0) == GL_TIMEOUT_EXPIRED) {
display_metrics_.announce_draw_status(
lines_submitted_,
std::chrono::high_resolution_clock::now() - line_submission_begin_time_,
false);
return;
}
fence_ = nullptr;
}
// Update the display metrics.
display_metrics_.announce_draw_status(
lines_submitted_,
std::chrono::high_resolution_clock::now() - line_submission_begin_time_,
true);
// Grab the new output list.
perform([&] {
const OutputArea area = get_output_area();
// Establish the pipeline if necessary.
const auto new_modals = BufferingScanTarget::new_modals();
if(bool(new_modals)) {
setup_pipeline();
}
// Determine the start time of this submission group and the number of lines it will contain.
line_submission_begin_time_ = std::chrono::high_resolution_clock::now();
lines_submitted_ = distance(area.begin.line, area.end.line, line_buffer_.size());
// Submit texture.
if(area.begin.write_area_x != area.end.write_area_x || area.begin.write_area_y != area.end.write_area_y) {
source_texture_.bind();
const auto submit = [&](const GLint y_begin, const GLint y_end) {
test_gl([&]{
glTexSubImage2D(
GL_TEXTURE_2D, 0,
0, y_begin,
WriteAreaWidth,
y_end - y_begin,
source_texture_.format(),
GL_UNSIGNED_BYTE,
&write_area_texture_[size_t(y_begin * WriteAreaWidth) * source_texture_.channels()]
);
});
};
// Both of the following upload to area.end.write_area_y + 1 to include whatever line the write area
// is currently on. It may have partial source areas along it, despite being incomplete.
if(area.end.write_area_y >= area.begin.write_area_y) {
// Submit the direct region from the submit pointer to the read pointer.
submit(area.begin.write_area_y, area.end.write_area_y + 1);
} else {
// The circular buffer wrapped around; submit the data from the read pointer to the end of
// the buffer and from the start of the buffer to the submit pointer.
submit(area.begin.write_area_y, WriteAreaHeight);
submit(0, area.end.write_area_y + 1);
}
}
test_gl([&]{ glDisable(GL_BLEND); });
test_gl([&]{ glDisable(GL_STENCIL_TEST); });
if(!is_rgb(existing_modals_->display_type)) {
process_to_rgb(area);
}
// Work with the accumulation_buffer_ potentially starts from here onwards; set its flag.
while(is_drawing_to_output_.test_and_set());
// Make sure there's an appropriately-sized buffer.
const auto output_buffer_width = output_width * 2;
const auto output_buffer_height = output_height * 2;
if(
output_buffers_[0].empty() ||
output_buffers_[0].width() != output_buffer_width ||
output_buffers_[0].height() != output_buffer_height
) {
const auto update = [&](const size_t index) {
TextureTarget new_output_buffer(
api_,
output_buffer_width,
output_buffer_height,
OutputTextureUnits[index],
GL_NEAREST,
true
);
// Resize old buffer into new.
if(!output_buffers_[index].empty()) {
new_output_buffer.bind_framebuffer();
output_buffers_[index].bind_texture();
copy_shader_.perform(OutputTextureUnits[index], 1.0f);
}
std::swap(output_buffers_[index], new_output_buffer);
};
update(0);
update(1);
update_aspect_ratio_transformation();
}
set_alphas();
test_gl([&]{ glEnable(GL_BLEND); });
test_gl([&]{ glEnable(GL_STENCIL_TEST); });
test_gl([&]{ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); });
if(!is_rgb(existing_modals_->display_type)) {
output_lines(area);
} else {
output_scans(area);
}
// That's it for operations affecting the accumulation buffer.
is_drawing_to_output_.clear();
// Grab a fence sync object to avoid busy waiting upon the next extry into this
// function, and reset the is_updating_ flag.
fence_ = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
complete_output_area(area);
});
}
void ScanTarget::process_to_rgb(const OutputArea &area) {
if(area.end.scan != area.begin.scan) {
// Submit all scans.
const auto new_scans = ::submit(scans_, area.begin.scan, area.end.scan, scan_buffer_);
// Populate composition buffer.
composition_buffer_.bind_framebuffer();
scans_.bind();
source_texture_.bind();
composition_shader_.bind();
test_gl([&]{ glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, GLsizei(new_scans)); });
}
// Do S-Video or composite line decoding.
if(area.end.line != area.begin.line) {
// Calculate and submit line dirty zones.
const int num_dirty_zones = 1 + (area.begin.line >= area.end.line);
dirty_zones_buffer_[0].begin = area.begin.line;
if(num_dirty_zones == 1) {
dirty_zones_buffer_[0].end = area.end.line;
} else {
dirty_zones_buffer_[0].end = LineBufferHeight;
dirty_zones_buffer_[1].begin = 0;
dirty_zones_buffer_[1].end = area.end.line;
}
dirty_zones_.bind_all();
test_gl([&]{
glBufferSubData(
GL_ARRAY_BUFFER,
0,
num_dirty_zones * sizeof(DirtyZone),
dirty_zones_buffer_.data()
);
});
// Perform [composite/svideo] -> RGB conversion.
composition_buffer_.bind_texture();
if(is_composite(existing_modals_->display_type)) {
separation_buffer_.bind_framebuffer();
separation_shader_.bind();
test_gl([&]{ glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, GLsizei(num_dirty_zones)); });
}
if(is_composite(existing_modals_->display_type) || is_svideo(existing_modals_->display_type)) {
demodulation_buffer_.bind_framebuffer();
demodulation_shader_.bind();
if(!separation_buffer_.empty()) {
separation_buffer_.bind_texture();
}
test_gl([&]{ glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, GLsizei(num_dirty_zones)); });
}
// Retroactively clear the composition buffer; doing this post hoc avoids uncertainty about the
// exact timing of a new line being drawn to, as well as fitting more neatly into when dirty zones
// are bound.
composition_buffer_.bind_framebuffer();
if(is_composite(existing_modals_->display_type)) {
fill_shader_.bind(0.0, 0.0, 0.0, 0.0);
} else {
fill_shader_.bind(0.0, 0.5, 0.5, 1.0);
}
test_gl([&]{ glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, GLsizei(num_dirty_zones)); });
}
}
void ScanTarget::bind_current_output_buffer() {
output_buffers_[output_buffer_ & (is_interlaced() ? 1 : 0)].bind_framebuffer();
}
void ScanTarget::toggle_output_buffer() {
output_buffer_ ^= 1;
bind_current_output_buffer();
}
void ScanTarget::output_lines(const OutputArea &area) {
bind_current_output_buffer();
BufferingScanTarget::output_lines(
area,
[&](const size_t begin, const size_t end) {
lines_.bind_all();
const auto new_lines = ::submit(lines_, begin, end, line_buffer_);
// Output new lines.
demodulation_buffer_.bind_texture();
line_output_shader_.bind();
test_gl([&]{ glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, GLsizei(new_lines)); });
},
[&](const bool was_complete) {
if(was_complete) {
full_display_rectangle_.draw(0.0, 0.0, 0.0);
}
test_gl([&]{ glClear(GL_STENCIL_BUFFER_BIT); });
toggle_output_buffer();
}
);
}
void ScanTarget::output_scans(const OutputArea &area) {
bind_current_output_buffer();
BufferingScanTarget::output_scans(
area,
[&](const size_t begin, const size_t end) {
scans_.bind_all();
const auto new_scans = ::submit(scans_, begin, end, scan_buffer_);
// Output new scans.
scan_output_shader_.bind();
composition_buffer_.bind_texture();
test_gl([&]{ glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, GLsizei(new_scans)); });
},
[&](const bool was_complete) {
if(was_complete) {
full_display_rectangle_.draw(0.0, 0.0, 0.0);
}
test_gl([&]{ glClear(GL_STENCIL_BUFFER_BIT); });
toggle_output_buffer();
}
);
}
void ScanTarget::draw(const int output_width, const int output_height) {
while(is_drawing_to_output_.test_and_set(std::memory_order_acquire));
test_gl([&]{ glBindFramebuffer(GL_FRAMEBUFFER, target_framebuffer_); });
test_gl([&]{ glViewport(0, 0, (GLsizei)output_width, (GLsizei)output_height); });
if(!output_buffers_[0].empty()) {
if(is_interlaced()) {
if(!was_interlacing_) {
output_buffers_[1].bind_framebuffer();
output_buffers_[0].bind_texture();
copy_shader_.perform(OutputTextureUnits[0], 1.0f);
}
test_gl([&]{ glEnable(GL_BLEND); });
test_gl([&]{ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); });
test_gl([&]{ glDisable(GL_STENCIL_TEST); });
output_buffers_[0].bind_texture();
copy_shader_.perform(OutputTextureUnits[0], 1.0f);
output_buffers_[1].bind_texture();
copy_shader_.perform(OutputTextureUnits[1], 0.5f);
} else {
test_gl([&]{ glDisable(GL_BLEND); });
test_gl([&]{ glDisable(GL_STENCIL_TEST); });
output_buffers_[0].bind_texture();
copy_shader_.perform(OutputTextureUnits[0], 1.0f);
}
was_interlacing_ = is_interlaced();
}
is_drawing_to_output_.clear(std::memory_order_release);
}