// // 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 "CompositionShader.hpp" #include "CopyShader.hpp" #include #include #include #include using namespace Outputs::Display::OpenGL; #ifndef NDEBUG //#define LOG_LINES //#define LOG_SCANS #endif namespace { /// The texture unit from which to source input data. constexpr GLenum SourceDataTextureUnit = GL_TEXTURE0; /// The texture unit which contains raw line-by-line composite, S-Video or RGB data. constexpr GLenum UnprocessedLineBufferTextureUnit = GL_TEXTURE1; /// The texture unit that contains a pre-lowpass-filtered but fixed-resolution version of the chroma signal; /// this is used when processing composite video only, and for chroma information only. Luminance is calculated /// at the fidelity permitted by the output target, but my efforts to separate, demodulate and filter /// chrominance during output without either massively sampling or else incurring significant high-frequency /// noise that sampling reduces into a Moire, have proven to be unsuccessful for the time being. constexpr GLenum QAMChromaTextureUnit = GL_TEXTURE2; /// The texture unit that contains the current display. constexpr GLenum AccumulationTextureUnit = GL_TEXTURE3; using Logger = Log::Logger; constexpr GLint internalFormatForDepth(const std::size_t depth) { switch(depth) { default: return GL_FALSE; case 1: return GL_R8UI; case 2: return GL_RG8UI; case 3: return GL_RGB8UI; case 4: return GL_RGBA8UI; } } constexpr GLenum formatForDepth(const std::size_t depth) { switch(depth) { default: return GL_FALSE; case 1: return GL_RED_INTEGER; case 2: return GL_RG_INTEGER; case 3: return GL_RGB_INTEGER; case 4: return GL_RGBA_INTEGER; } } template void allocate_buffer( const T &array, GLuint &buffer_name, GLuint &vertex_array_name ) { const auto buffer_size = array.size() * sizeof(array[0]); test_gl([&]{ glGenBuffers(1, &buffer_name); }); test_gl([&]{ glBindBuffer(GL_ARRAY_BUFFER, buffer_name); }); test_gl([&]{ glBufferData(GL_ARRAY_BUFFER, GLsizeiptr(buffer_size), NULL, GL_STREAM_DRAW); }); test_gl([&]{ glGenVertexArrays(1, &vertex_array_name); }); test_gl([&]{ glBindVertexArray(vertex_array_name); }); test_gl([&]{ glBindBuffer(GL_ARRAY_BUFFER, buffer_name); }); } } ScanTarget::ScanTarget(const API api, const GLuint target_framebuffer, const float output_gamma) : api_(api), target_framebuffer_(target_framebuffer), output_gamma_(output_gamma), unprocessed_line_texture_(api, LineBufferWidth, LineBufferHeight, UnprocessedLineBufferTextureUnit, GL_NEAREST, false), full_display_rectangle_(api, -1.0f, -1.0f, 2.0f, 2.0f), scans_(scan_buffer_) { set_scan_buffer(scan_buffer_.data(), scan_buffer_.size()); set_line_buffer(line_buffer_.data(), line_metadata_buffer_.data(), line_buffer_.size()); // Allocate space for the scans and lines. allocate_buffer(scan_buffer_, scan_buffer_name_, scan_vertex_array_); allocate_buffer(line_buffer_, line_buffer_name_, line_vertex_array_); // 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. test_gl([&]{ glBlendFunc(GL_SRC_ALPHA, GL_CONSTANT_COLOR); }); test_gl([&]{ glBlendColor(0.4f, 0.4f, 0.4f, 1.0f); }); // Establish initial state for is_drawing_to_accumulation_buffer_. is_drawing_to_accumulation_buffer_.clear(); // TEST CODE. NOCOMMIT. // const auto buffer_width = FilterGenerator::SuggestedBufferWidth; // const float sample_multiplier = // FilterGenerator::suggested_sample_multiplier(227.5f, 1320); // // VertexArray va(scan_buffer_); // for(auto &pair: { // std::make_pair(InputDataType::Luminance1, DisplayType::CompositeColour), // std::make_pair(InputDataType::Luminance8, DisplayType::CompositeColour), // std::make_pair(InputDataType::PhaseLinkedLuminance8, DisplayType::CompositeColour), // // std::make_pair(InputDataType::Luminance8Phase8, DisplayType::SVideo), // std::make_pair(InputDataType::Luminance8Phase8, DisplayType::CompositeColour), // // std::make_pair(InputDataType::Red1Green1Blue1, DisplayType::RGB), // std::make_pair(InputDataType::Red1Green1Blue1, DisplayType::SVideo), // std::make_pair(InputDataType::Red1Green1Blue1, DisplayType::CompositeColour), // // std::make_pair(InputDataType::Red2Green2Blue2, DisplayType::RGB), // std::make_pair(InputDataType::Red2Green2Blue2, DisplayType::SVideo), // std::make_pair(InputDataType::Red2Green2Blue2, DisplayType::CompositeColour), // // std::make_pair(InputDataType::Red4Green4Blue4, DisplayType::RGB), // std::make_pair(InputDataType::Red4Green4Blue4, DisplayType::SVideo), // std::make_pair(InputDataType::Red4Green4Blue4, DisplayType::CompositeColour), // // std::make_pair(InputDataType::Red8Green8Blue8, DisplayType::RGB), // std::make_pair(InputDataType::Red8Green8Blue8, DisplayType::SVideo), // std::make_pair(InputDataType::Red8Green8Blue8, DisplayType::CompositeColour), // }) { // OpenGL::composition_shader( // api, // pair.first, // pair.second, // ColourSpace::YIQ, // sample_multiplier, // BufferingScanTarget::WriteAreaWidth, BufferingScanTarget::WriteAreaHeight, // buffer_width, 2048, // TODO: substitute real composition buffer sizes. // va, // GL_TEXTURE0 // ).bind(); // } } ScanTarget::~ScanTarget() { perform([&] { glDeleteBuffers(1, &scan_buffer_name_); glDeleteVertexArrays(1, &scan_vertex_array_); }); } void ScanTarget::set_target_framebuffer(GLuint target_framebuffer) { perform([&] { target_framebuffer_ = target_framebuffer; }); } 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 only 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()); } // Prepare to bind line shaders. test_gl([&]{ glBindVertexArray(line_vertex_array_); }); test_gl([&]{ glBindBuffer(GL_ARRAY_BUFFER, line_buffer_name_); }); // Destroy or create a QAM buffer and shader, if appropriate. if(!existing_modals_ || existing_modals_->display_type != modals.display_type) { const bool needs_qam_buffer = modals.display_type == DisplayType::CompositeColour || modals.display_type == DisplayType::SVideo; if(needs_qam_buffer) { if(!qam_chroma_texture_) { qam_chroma_texture_ = std::make_unique( api_, LineBufferWidth, LineBufferHeight, QAMChromaTextureUnit, GL_NEAREST, false ); } qam_separation_shader_ = qam_separation_shader(); enable_vertex_attributes(ShaderType::QAMSeparation, *qam_separation_shader_); qam_separation_shader_->set_uniform("textureName", GLint(UnprocessedLineBufferTextureUnit - GL_TEXTURE0)); } else { qam_chroma_texture_.reset(); qam_separation_shader_.reset(); } // Establish an output shader. output_shader_ = conversion_shader(); enable_vertex_attributes(ShaderType::Conversion, *output_shader_); set_uniforms(ShaderType::Conversion, *output_shader_); output_shader_->set_uniform("textureName", GLint(UnprocessedLineBufferTextureUnit - GL_TEXTURE0)); output_shader_->set_uniform("qamTextureName", GLint(QAMChromaTextureUnit - GL_TEXTURE0)); } if(qam_separation_shader_) { set_uniforms(ShaderType::QAMSeparation, *qam_separation_shader_); } // Select whichever of a letterbox or pillarbox avoids cropping. constexpr float output_ratio = 4.0f / 3.0f; const float aspect_ratio_stretch = modals.aspect_ratio / output_ratio; auto adjusted_rect = modals.visible_area; const float letterbox_scale = adjusted_rect.size.height / (adjusted_rect.size.width * aspect_ratio_stretch); if(letterbox_scale > 1.0f) { adjusted_rect.scale(letterbox_scale, 1.0f); } else { adjusted_rect.scale(1.0f, 1.0f / letterbox_scale); } // Provide to shader. output_shader_->set_uniform("origin", adjusted_rect.origin.x, adjusted_rect.origin.y); output_shader_->set_uniform("size", 1.0f / adjusted_rect.size.width, 1.0f / adjusted_rect.size.height); // Establish an input shader. if(!existing_modals_ || existing_modals_->input_data_type != modals.input_data_type) { input_shader_ = composition_shader(); test_gl([&]{ glBindVertexArray(scan_vertex_array_); }); test_gl([&]{ glBindBuffer(GL_ARRAY_BUFFER, scan_buffer_name_); }); enable_vertex_attributes(ShaderType::Composition, *input_shader_); set_uniforms(ShaderType::Composition, *input_shader_); input_shader_->set_uniform("textureName", GLint(SourceDataTextureUnit - GL_TEXTURE0)); } // // New pipeline starts here! // const auto buffer_width = FilterGenerator::SuggestedBufferWidth; const float sample_multiplier = FilterGenerator::suggested_sample_multiplier(227.5f, 1320); if(copy_shader_.empty()) { copy_shader_ = copy_shader(api_, GL_TEXTURE4, {}, {}); } 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 ) { // TODO: a temporary vertex array is created here for now, to avoid messing // with the bindings of the real scan vertex array. Move past that. VertexArray array(scan_buffer_); composition_shader_ = OpenGL::composition_shader( api_, modals.input_data_type, modals.display_type, modals.composite_colour_space, sample_multiplier, 2048, 2048, buffer_width, 2048, array, GL_TEXTURE0 ); } if(composition_buffer_.empty()) { composition_buffer_ = TextureTarget(api_, buffer_width, 2048, GL_TEXTURE4, GL_NEAREST, false); composition_buffer_.bind_texture(); // The below establishes that the composition buffer texture is ultimately being drawn. std::vector image(buffer_width * 2048 * 4); for(auto &c : image) { c = rand(); } test_gl([&]{ glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, buffer_width, 2048, 0, GL_RGBA, GL_UNSIGNED_BYTE, image.data() ); }); } 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(int, 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(); const bool did_setup_pipeline = [&] { if(bool(new_modals)) { setup_pipeline(); return true; } return false; } (); // 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_ = (area.end.line - area.start.line + line_buffer_.size()) % line_buffer_.size(); // Submit texture. if(area.start.write_area_x != area.end.write_area_x || area.start.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, formatForDepth(write_area_data_size()), 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.start.write_area_y) { // Submit the direct region from the submit pointer to the read pointer. submit(area.start.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.start.write_area_y, WriteAreaHeight); submit(0, area.end.write_area_y + 1); } } // Submit scans; only the new ones need to be communicated. if(area.end.scan != area.start.scan) { const size_t new_scans = (area.end.scan - area.start.scan + scan_buffer_.size()) % scan_buffer_.size(); // // OLD PIPELINE: submit new scans. // test_gl([&]{ glBindBuffer(GL_ARRAY_BUFFER, scan_buffer_name_); }); // Map only the required portion of the buffer. const size_t new_scans_size = new_scans * sizeof(Scan); const auto destination = static_cast( glMapBufferRange( GL_ARRAY_BUFFER, 0, GLsizeiptr(new_scans_size), GL_MAP_WRITE_BIT | GL_MAP_FLUSH_EXPLICIT_BIT ) ); test_gl_error(); // Copy as a single chunk if possible; otherwise copy in two parts. if(area.start.scan < area.end.scan) { std::copy_n(scan_buffer_.begin() + area.start.scan, new_scans, destination); } else { const size_t first_portion_count = scan_buffer_.size() - area.start.scan; std::copy_n(scan_buffer_.begin() + area.start.scan, first_portion_count, destination); std::copy_n(scan_buffer_.begin(), new_scans - first_portion_count, destination + first_portion_count); } // Flush and unmap the buffer. test_gl([&]{ glFlushMappedBufferRange(GL_ARRAY_BUFFER, 0, GLsizeiptr(new_scans_size)); }); test_gl([&]{ glUnmapBuffer(GL_ARRAY_BUFFER); }); // // OLD PIPELINE: draw new scans. // // Push new input to the unprocessed line buffer. unprocessed_line_texture_.bind_framebuffer(); // Clear newly-touched lines; that is everything from (read+1) to submit. const auto first_line_to_clear = GLsizei((area.start.line+1)%line_buffer_.size()); const auto final_line_to_clear = GLsizei(area.end.line); if(first_line_to_clear != final_line_to_clear) { test_gl([&]{ glEnable(GL_SCISSOR_TEST); }); // Determine the proper clear colour — this needs to be anything that describes black // in the input colour encoding at use. if(modals().input_data_type == InputDataType::Luminance8Phase8) { // Supply both a zero luminance and a colour-subcarrier-disengaging phase. test_gl([&]{ glClearColor(0.0f, 1.0f, 0.0f, 0.0f); }); } else { test_gl([&]{ glClearColor(0.0f, 0.0f, 0.0f, 0.0f); }); } if(first_line_to_clear < final_line_to_clear) { test_gl([&]{ glScissor(GLint(0), GLint(first_line_to_clear), unprocessed_line_texture_.width(), final_line_to_clear - first_line_to_clear); }); test_gl([&]{ glClear(GL_COLOR_BUFFER_BIT); }); } else { test_gl([&]{ glScissor(GLint(0), GLint(0), unprocessed_line_texture_.width(), final_line_to_clear); }); test_gl([&]{ glClear(GL_COLOR_BUFFER_BIT); }); test_gl([&]{ glScissor(GLint(0), GLint(first_line_to_clear), unprocessed_line_texture_.width(), unprocessed_line_texture_.height() - first_line_to_clear); }); test_gl([&]{ glClear(GL_COLOR_BUFFER_BIT); }); } test_gl([&]{ glDisable(GL_SCISSOR_TEST); }); } // Apply new spans. They definitely always go to the first buffer. test_gl([&]{ glBindVertexArray(scan_vertex_array_); }); input_shader_->bind(); test_gl([&]{ glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, GLsizei(new_scans)); }); // // NEW PIPELINE. // // Submit new scans. // First implementation: put all new scans at the start of the buffer, for a simple // glDrawArraysInstanced call below. scans_.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(Scan), &scan_buffer_[begin] ); }); buffer_destination += (end - begin) * sizeof(Scan); }; if(area.start.scan < area.end.scan) { submit(area.start.scan, area.end.scan); } else { submit(area.start.scan, scan_buffer_.size()); submit(0, area.end.scan); } // Populate composition buffer. composition_buffer_.bind_framebuffer(); scans_.bind(); composition_shader_.bind(); test_gl([&]{ glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, GLsizei(new_scans)); }); } // Logic for reducing resolution: start doing so if the metrics object reports that // it's a good idea. Go up to a quarter of the requested resolution, subject to // clamping at each stage. If the output resolution changes, or anything else about // the output pipeline, just start trying the highest size again. if(display_metrics_.should_lower_resolution() && is_soft_display_type()) { resolution_reduction_level_ = std::min(resolution_reduction_level_+1, 4); } if(output_height_ != output_height || did_setup_pipeline) { resolution_reduction_level_ = 1; output_height_ = output_height; } // Ensure the accumulation buffer is properly sized, allowing for the metrics object's // feelings about whether too high a resolution is being used. const int framebuffer_height = std::max(output_height / resolution_reduction_level_, std::min(540, output_height)); const int proportional_width = (framebuffer_height * 4) / 3; const bool did_create_accumulation_texture = !accumulation_texture_ || ( accumulation_texture_->width() != proportional_width || accumulation_texture_->height() != framebuffer_height ); // Work with the accumulation_buffer_ potentially starts from here onwards; set its flag. while(is_drawing_to_accumulation_buffer_.test_and_set()); if(did_create_accumulation_texture) { Logger::info().append("Changed output resolution to %d by %d", proportional_width, framebuffer_height); display_metrics_.announce_did_resize(); auto new_framebuffer = std::make_unique( api_, GLsizei(proportional_width), GLsizei(framebuffer_height), AccumulationTextureUnit, GL_NEAREST, true ); if(accumulation_texture_) { new_framebuffer->bind_framebuffer(); test_gl([&]{ glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); }); test_gl([&]{ glActiveTexture(AccumulationTextureUnit); }); accumulation_texture_->bind_texture(); accumulation_texture_->draw(4.0f / 3.0f); test_gl([&]{ glClear(GL_STENCIL_BUFFER_BIT); }); new_framebuffer->bind_texture(); } accumulation_texture_ = std::move(new_framebuffer); // In the absence of a way to resize a stencil buffer, just mark // what's currently present as invalid to avoid an improper clear // for this frame. stencil_is_valid_ = false; } if(did_setup_pipeline || did_create_accumulation_texture) { set_sampling_window(proportional_width, framebuffer_height, *output_shader_); } // Figure out how many new lines are ready. auto new_lines = (area.end.line - area.start.line + LineBufferHeight) % LineBufferHeight; if(new_lines) { // Prepare to output lines. test_gl([&]{ glBindVertexArray(line_vertex_array_); }); // Bind the accumulation framebuffer, unless there's going to be QAM work first. if(!qam_separation_shader_ || line_metadata_buffer_[area.start.line].is_first_in_frame) { accumulation_texture_->bind_framebuffer(); output_shader_->bind(); // Enable blending and stenciling. test_gl([&]{ glEnable(GL_BLEND); }); test_gl([&]{ glEnable(GL_STENCIL_TEST); }); } // Set the proper stencil function regardless. test_gl([&]{ glStencilFunc(GL_EQUAL, 0, GLuint(~0)); }); test_gl([&]{ glStencilOp(GL_KEEP, GL_KEEP, GL_INCR); }); // Prepare to upload data that will consitute lines. test_gl([&]{ glBindBuffer(GL_ARRAY_BUFFER, line_buffer_name_); }); // Divide spans by which frame they're in. auto start_line = area.start.line; while(new_lines) { uint16_t end_line = (start_line + 1) % LineBufferHeight; // Find the limit of spans to draw in this cycle. size_t lines = 1; while(end_line != area.end.line && !line_metadata_buffer_[end_line].is_first_in_frame) { end_line = (end_line + 1) % LineBufferHeight; ++lines; } // If this is start-of-frame, clear any untouched pixels and flush the stencil buffer if(line_metadata_buffer_[start_line].is_first_in_frame) { if(stencil_is_valid_ && line_metadata_buffer_[start_line].previous_frame_was_complete) { full_display_rectangle_.draw(0.0f, 0.0f, 0.0f); } stencil_is_valid_ = true; test_gl([&]{ glClear(GL_STENCIL_BUFFER_BIT); }); // Rebind the program for span output. test_gl([&]{ glBindVertexArray(line_vertex_array_); }); if(!qam_separation_shader_) { output_shader_->bind(); } } // Upload. const auto buffer_size = lines * sizeof(Line); if(!end_line || end_line > start_line) { test_gl([&]{ glBufferSubData(GL_ARRAY_BUFFER, 0, GLsizeiptr(buffer_size), &line_buffer_[start_line]); }); } else { auto destination = static_cast( glMapBufferRange( GL_ARRAY_BUFFER, 0, GLsizeiptr(buffer_size), GL_MAP_WRITE_BIT | GL_MAP_FLUSH_EXPLICIT_BIT ) ); assert(destination); test_gl_error(); std::copy_n(line_buffer_.begin(), end_line, destination + line_buffer_.size() - start_line); std::copy_n(line_buffer_.begin() + start_line, line_buffer_.size() - start_line, destination); test_gl([&]{ glFlushMappedBufferRange(GL_ARRAY_BUFFER, 0, GLsizeiptr(buffer_size)); }); test_gl([&]{ glUnmapBuffer(GL_ARRAY_BUFFER); }); } // Produce colour information, if required. if(qam_separation_shader_) { qam_separation_shader_->bind(); qam_chroma_texture_->bind_framebuffer(); test_gl([&]{ glClear(GL_COLOR_BUFFER_BIT); }); // TODO: this is here as a hint that the old framebuffer doesn't need reloading; // test whether that's a valid optimisation on desktop OpenGL. test_gl([&]{ glDisable(GL_BLEND); }); test_gl([&]{ glDisable(GL_STENCIL_TEST); }); test_gl([&]{ glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, GLsizei(lines)); }); accumulation_texture_->bind_framebuffer(); output_shader_->bind(); test_gl([&]{ glEnable(GL_BLEND); }); test_gl([&]{ glEnable(GL_STENCIL_TEST); }); } // Render to the output. test_gl([&]{ glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, GLsizei(lines)); }); start_line = end_line; new_lines -= lines; } // Disable blending and the stencil test again. test_gl([&]{ glDisable(GL_STENCIL_TEST); }); test_gl([&]{ glDisable(GL_BLEND); }); } // That's it for operations affecting the accumulation buffer. is_drawing_to_accumulation_buffer_.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::draw(int output_width, int output_height) { while(is_drawing_to_accumulation_buffer_.test_and_set(std::memory_order_acquire)); if(accumulation_texture_) { // Copy the accumulation texture to the target. test_gl([&]{ glBindFramebuffer(GL_FRAMEBUFFER, target_framebuffer_); }); test_gl([&]{ glViewport(0, 0, (GLsizei)output_width, (GLsizei)output_height); }); test_gl([&]{ glClearColor(0.0f, 0.0f, 0.0f, 0.0f); }); test_gl([&]{ glClear(GL_COLOR_BUFFER_BIT); }); accumulation_texture_->bind_texture(); accumulation_texture_->draw(float(output_width) / float(output_height), 4.0f / 255.0f); } if(!composition_buffer_.empty()) { // Copy the accumulation texture to the target. test_gl([&]{ glBindFramebuffer(GL_FRAMEBUFFER, target_framebuffer_); }); test_gl([&]{ glViewport(0, 0, (GLsizei)output_width, (GLsizei)output_height); }); composition_buffer_.draw(float(output_width) / float(output_height), 4.0f / 255.0f); } is_drawing_to_accumulation_buffer_.clear(std::memory_order_release); }