diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index 62bfff491..9f491a899 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -786,6 +786,8 @@ 4BC1317B2346DF2B00E4FF3D /* MSA.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC131782346DF2B00E4FF3D /* MSA.cpp */; }; 4BC23A2C2467600F001A6030 /* OPLL.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC23A2B2467600E001A6030 /* OPLL.cpp */; }; 4BC23A2D2467600F001A6030 /* OPLL.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC23A2B2467600E001A6030 /* OPLL.cpp */; }; + 4BC3C67C24C9230F0027BF76 /* BufferingScanTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC3C67A24C9230F0027BF76 /* BufferingScanTarget.cpp */; }; + 4BC3C67D24C9230F0027BF76 /* BufferingScanTarget.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC3C67A24C9230F0027BF76 /* BufferingScanTarget.cpp */; }; 4BC57CD92436A62900FBC404 /* State.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC57CD82436A62900FBC404 /* State.cpp */; }; 4BC57CDA2436A62900FBC404 /* State.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC57CD82436A62900FBC404 /* State.cpp */; }; 4BC5C3E022C994CD00795658 /* 68000MoveTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4BC5C3DF22C994CC00795658 /* 68000MoveTests.mm */; }; @@ -1668,6 +1670,8 @@ 4BC23A292467600E001A6030 /* OPLBase.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = OPLBase.hpp; sourceTree = ""; }; 4BC23A2A2467600E001A6030 /* EnvelopeGenerator.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = EnvelopeGenerator.hpp; sourceTree = ""; }; 4BC23A2B2467600E001A6030 /* OPLL.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = OPLL.cpp; sourceTree = ""; }; + 4BC3C67A24C9230F0027BF76 /* BufferingScanTarget.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = BufferingScanTarget.cpp; path = ../../Outputs/ScanTargets/BufferingScanTarget.cpp; sourceTree = ""; }; + 4BC3C67B24C9230F0027BF76 /* BufferingScanTarget.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = BufferingScanTarget.hpp; path = ../../Outputs/ScanTargets/BufferingScanTarget.hpp; sourceTree = ""; }; 4BC57CD2243427C700FBC404 /* AudioProducer.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = AudioProducer.hpp; sourceTree = ""; }; 4BC57CD32434282000FBC404 /* TimedMachine.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = TimedMachine.hpp; sourceTree = ""; }; 4BC57CD424342E0600FBC404 /* MachineTypes.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MachineTypes.hpp; sourceTree = ""; }; @@ -3307,6 +3311,8 @@ 4BB73E951B587A5100552FC2 = { isa = PBXGroup; children = ( + 4BC3C67A24C9230F0027BF76 /* BufferingScanTarget.cpp */, + 4BC3C67B24C9230F0027BF76 /* BufferingScanTarget.hpp */, 4BC76E6A1C98F43700E6EF73 /* Accelerate.framework */, 4B51F70820A521D700AFA2C1 /* Activity */, 4B8944E2201967B4007DE474 /* Analyser */, @@ -4485,6 +4491,7 @@ 4BEBFB522002DB30000708CC /* DiskROM.cpp in Sources */, 4BC23A2D2467600F001A6030 /* OPLL.cpp in Sources */, 4B055AA11FAE85DA0060FFFF /* OricMFMDSK.cpp in Sources */, + 4BC3C67D24C9230F0027BF76 /* BufferingScanTarget.cpp in Sources */, 4B0ACC2923775819008902D0 /* DMAController.cpp in Sources */, 4B055A951FAE85BB0060FFFF /* BitReverse.cpp in Sources */, 4B055ACE1FAE9B030060FFFF /* Plus3.cpp in Sources */, @@ -4631,6 +4638,7 @@ 4B3BF5B01F146265005B6C36 /* CSW.cpp in Sources */, 4BCE0060227D39AB000CA200 /* Video.cpp in Sources */, 4B0ACC2E23775819008902D0 /* TIA.cpp in Sources */, + 4BC3C67C24C9230F0027BF76 /* BufferingScanTarget.cpp in Sources */, 4B74CF85231370BC00500CE8 /* MacintoshVolume.cpp in Sources */, 4B4518A51F75FD1C00926311 /* SSD.cpp in Sources */, 4B55CE5F1C3B7D960093A61B /* MachineDocument.swift in Sources */, diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme index 63be2c037..a99eb0a5d 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme +++ b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme @@ -67,7 +67,7 @@ > 11) -#define TextureAddressGetX(v) uint16_t((v) & 0x7ff) -#define TextureSub(a, b) (((a) - (b)) & 0x3fffff) - constexpr GLint internalFormatForDepth(std::size_t depth) { switch(depth) { default: return GL_FALSE; @@ -84,9 +79,8 @@ ScanTarget::ScanTarget(GLuint target_framebuffer, float output_gamma) : unprocessed_line_texture_(LineBufferWidth, LineBufferHeight, UnprocessedLineBufferTextureUnit, GL_NEAREST, false), full_display_rectangle_(-1.0f, -1.0f, 2.0f, 2.0f) { - // Ensure proper initialisation of the two atomic pointer sets. - read_pointers_.store(write_pointers_); - submit_pointers_.store(write_pointers_); + 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_); @@ -101,265 +95,31 @@ ScanTarget::ScanTarget(GLuint target_framebuffer, float output_gamma) : test_gl(glBlendFunc, GL_SRC_ALPHA, GL_CONSTANT_COLOR); test_gl(glBlendColor, 0.4f, 0.4f, 0.4f, 1.0f); - // Establish initial state for the two atomic flags. - is_updating_.clear(); + // Establish initial state for is_drawing_to_accumulation_buffer_. is_drawing_to_accumulation_buffer_.clear(); } ScanTarget::~ScanTarget() { - while(is_updating_.test_and_set()); - glDeleteBuffers(1, &scan_buffer_name_); - glDeleteTextures(1, &write_area_texture_name_); - glDeleteVertexArrays(1, &scan_vertex_array_); + perform([=] { + glDeleteBuffers(1, &scan_buffer_name_); + glDeleteTextures(1, &write_area_texture_name_); + glDeleteVertexArrays(1, &scan_vertex_array_); + }); } void ScanTarget::set_target_framebuffer(GLuint target_framebuffer) { - while(is_updating_.test_and_set()); - target_framebuffer_ = target_framebuffer; - is_updating_.clear(); -} - -void ScanTarget::set_modals(Modals modals) { - // Don't change the modals while drawing is ongoing; a previous set might be - // in the process of being established. - while(is_updating_.test_and_set()); - modals_ = modals; - modals_are_dirty_ = true; - is_updating_.clear(); -} - -Outputs::Display::ScanTarget::Scan *ScanTarget::begin_scan() { - if(allocation_has_failed_) return nullptr; - - std::lock_guard lock_guard(write_pointers_mutex_); - - const auto result = &scan_buffer_[write_pointers_.scan_buffer]; - const auto read_pointers = read_pointers_.load(); - - // Advance the pointer. - const auto next_write_pointer = decltype(write_pointers_.scan_buffer)((write_pointers_.scan_buffer + 1) % scan_buffer_.size()); - - // Check whether that's too many. - if(next_write_pointer == read_pointers.scan_buffer) { - allocation_has_failed_ = true; - return nullptr; - } - write_pointers_.scan_buffer = next_write_pointer; - ++provided_scans_; - - // Fill in extra OpenGL-specific details. - result->line = write_pointers_.line; - - vended_scan_ = result; - return &result->scan; -} - -void ScanTarget::end_scan() { - if(vended_scan_) { - std::lock_guard lock_guard(write_pointers_mutex_); - vended_scan_->data_y = TextureAddressGetY(vended_write_area_pointer_); - vended_scan_->line = write_pointers_.line; - vended_scan_->scan.end_points[0].data_offset += TextureAddressGetX(vended_write_area_pointer_); - vended_scan_->scan.end_points[1].data_offset += TextureAddressGetX(vended_write_area_pointer_); - -#ifdef LOG_SCANS - if(vended_scan_->scan.composite_amplitude) { - std::cout << "S: "; - std::cout << vended_scan_->scan.end_points[0].composite_angle << "/" << vended_scan_->scan.end_points[0].data_offset << "/" << vended_scan_->scan.end_points[0].cycles_since_end_of_horizontal_retrace << " -> "; - std::cout << vended_scan_->scan.end_points[1].composite_angle << "/" << vended_scan_->scan.end_points[1].data_offset << "/" << vended_scan_->scan.end_points[1].cycles_since_end_of_horizontal_retrace << " => "; - std::cout << double(vended_scan_->scan.end_points[1].composite_angle - vended_scan_->scan.end_points[0].composite_angle) / (double(vended_scan_->scan.end_points[1].data_offset - vended_scan_->scan.end_points[0].data_offset) * 64.0f) << "/"; - std::cout << double(vended_scan_->scan.end_points[1].composite_angle - vended_scan_->scan.end_points[0].composite_angle) / (double(vended_scan_->scan.end_points[1].cycles_since_end_of_horizontal_retrace - vended_scan_->scan.end_points[0].cycles_since_end_of_horizontal_retrace) * 64.0f); - std::cout << std::endl; - } -#endif - } - vended_scan_ = nullptr; -} - -uint8_t *ScanTarget::begin_data(size_t required_length, size_t required_alignment) { - assert(required_alignment); - - if(allocation_has_failed_) return nullptr; - - std::lock_guard lock_guard(write_pointers_mutex_); - if(write_area_texture_.empty()) { - allocation_has_failed_ = true; - return nullptr; - } - - // Determine where the proposed write area would start and end. - uint16_t output_y = TextureAddressGetY(write_pointers_.write_area); - - uint16_t aligned_start_x = TextureAddressGetX(write_pointers_.write_area & 0xffff) + 1; - aligned_start_x += uint16_t((required_alignment - aligned_start_x%required_alignment)%required_alignment); - - uint16_t end_x = aligned_start_x + uint16_t(1 + required_length); - - if(end_x > WriteAreaWidth) { - output_y = (output_y + 1) % WriteAreaHeight; - aligned_start_x = uint16_t(required_alignment); - end_x = aligned_start_x + uint16_t(1 + required_length); - } - - // Check whether that steps over the read pointer. - const auto end_address = TextureAddress(end_x, output_y); - const auto read_pointers = read_pointers_.load(); - - const auto end_distance = TextureSub(end_address, read_pointers.write_area); - const auto previous_distance = TextureSub(write_pointers_.write_area, read_pointers.write_area); - - // If allocating this would somehow make the write pointer back away from the read pointer, - // there must not be enough space left. - if(end_distance < previous_distance) { - allocation_has_failed_ = true; - return nullptr; - } - - // Everything checks out, note expectation of a future end_data and return the pointer. - data_is_allocated_ = true; - vended_write_area_pointer_ = write_pointers_.write_area = TextureAddress(aligned_start_x, output_y); - - assert(write_pointers_.write_area >= 1 && ((size_t(write_pointers_.write_area) + required_length + 1) * data_type_size_) <= write_area_texture_.size()); - return &write_area_texture_[size_t(write_pointers_.write_area) * data_type_size_]; - - // Note state at exit: - // write_pointers_.write_area points to the first pixel the client is expected to draw to. -} - -void ScanTarget::end_data(size_t actual_length) { - if(allocation_has_failed_ || !data_is_allocated_) return; - - std::lock_guard lock_guard(write_pointers_mutex_); - - // Bookend the start of the new data, to safeguard for precision errors in sampling. - memcpy( - &write_area_texture_[size_t(write_pointers_.write_area - 1) * data_type_size_], - &write_area_texture_[size_t(write_pointers_.write_area) * data_type_size_], - data_type_size_); - - // Advance to the end of the current run. - write_pointers_.write_area += actual_length + 1; - - // Also bookend the end. - memcpy( - &write_area_texture_[size_t(write_pointers_.write_area - 1) * data_type_size_], - &write_area_texture_[size_t(write_pointers_.write_area - 2) * data_type_size_], - data_type_size_); - - // The write area was allocated in the knowledge that there's sufficient - // distance left on the current line, but there's a risk of exactly filling - // the final line, in which case this should wrap back to 0. - write_pointers_.write_area %= (write_area_texture_.size() / data_type_size_); - - // Record that no further end_data calls are expected. - data_is_allocated_ = false; -} - -void ScanTarget::will_change_owner() { - allocation_has_failed_ = true; - vended_scan_ = nullptr; -} - -void ScanTarget::announce(Event event, bool is_visible, const Outputs::Display::ScanTarget::Scan::EndPoint &location, uint8_t composite_amplitude) { - // Forward the event to the display metrics tracker. - display_metrics_.announce_event(event); - - if(event == ScanTarget::Event::EndVerticalRetrace) { - // The previous-frame-is-complete flag is subject to a two-slot queue because - // measurement for *this* frame needs to begin now, meaning that the previous - // result needs to be put somewhere — it'll be attached to the first successful - // line output. - is_first_in_frame_ = true; - previous_frame_was_complete_ = frame_is_complete_; - frame_is_complete_ = true; - } - - if(output_is_visible_ == is_visible) return; - if(is_visible) { - const auto read_pointers = read_pointers_.load(); - std::lock_guard lock_guard(write_pointers_mutex_); - - // Commit the most recent line only if any scans fell on it. - // Otherwise there's no point outputting it, it'll contribute nothing. - if(provided_scans_) { - // Store metadata if concluding a previous line. - if(active_line_) { - line_metadata_buffer_[size_t(write_pointers_.line)].is_first_in_frame = is_first_in_frame_; - line_metadata_buffer_[size_t(write_pointers_.line)].previous_frame_was_complete = previous_frame_was_complete_; - is_first_in_frame_ = false; - } - - // Attempt to allocate a new line; note allocation failure if necessary. - const auto next_line = uint16_t((write_pointers_.line + 1) % LineBufferHeight); - if(next_line == read_pointers.line) { - allocation_has_failed_ = true; - active_line_ = nullptr; - } else { - write_pointers_.line = next_line; - active_line_ = &line_buffer_[size_t(write_pointers_.line)]; - } - provided_scans_ = 0; - } - - if(active_line_) { - active_line_->end_points[0].x = location.x; - active_line_->end_points[0].y = location.y; - active_line_->end_points[0].cycles_since_end_of_horizontal_retrace = location.cycles_since_end_of_horizontal_retrace; - active_line_->end_points[0].composite_angle = location.composite_angle; - active_line_->line = write_pointers_.line; - active_line_->composite_amplitude = composite_amplitude; - } - } else { - if(active_line_) { - // A successfully-allocated line is ending. - active_line_->end_points[1].x = location.x; - active_line_->end_points[1].y = location.y; - active_line_->end_points[1].cycles_since_end_of_horizontal_retrace = location.cycles_since_end_of_horizontal_retrace; - active_line_->end_points[1].composite_angle = location.composite_angle; - -#ifdef LOG_LINES - if(active_line_->composite_amplitude) { - std::cout << "L: "; - std::cout << active_line_->end_points[0].composite_angle << "/" << active_line_->end_points[0].cycles_since_end_of_horizontal_retrace << " -> "; - std::cout << active_line_->end_points[1].composite_angle << "/" << active_line_->end_points[1].cycles_since_end_of_horizontal_retrace << " => "; - std::cout << (active_line_->end_points[1].composite_angle - active_line_->end_points[0].composite_angle) << "/" << (active_line_->end_points[1].cycles_since_end_of_horizontal_retrace - active_line_->end_points[0].cycles_since_end_of_horizontal_retrace) << " => "; - std::cout << double(active_line_->end_points[1].composite_angle - active_line_->end_points[0].composite_angle) / (double(active_line_->end_points[1].cycles_since_end_of_horizontal_retrace - active_line_->end_points[0].cycles_since_end_of_horizontal_retrace) * 64.0f); - std::cout << std::endl; - } -#endif - } - - // A line is complete; submit latest updates if nothing failed. - if(allocation_has_failed_) { - // Reset all pointers to where they were; this also means - // the stencil won't be properly populated. - write_pointers_ = submit_pointers_.load(); - frame_is_complete_ = false; - } else { - // Advance submit pointer. - submit_pointers_.store(write_pointers_); - } - allocation_has_failed_ = false; - } - output_is_visible_ = is_visible; + perform([=] { + target_framebuffer_ = target_framebuffer; + }); } void ScanTarget::setup_pipeline() { const auto data_type_size = Outputs::Display::size_for_data_type(modals_.input_data_type); - // Ensure the lock guard here has a restricted scope; this is the only time that a thread - // other than the main owner of write_pointers_ may adjust it. - { - std::lock_guard lock_guard(write_pointers_mutex_); - if(data_type_size != data_type_size_) { - // TODO: flush output. - - data_type_size_ = data_type_size; - write_area_texture_.resize(WriteAreaWidth*WriteAreaHeight*data_type_size_); - - write_pointers_.scan_buffer = 0; - write_pointers_.write_area = 0; - } + // Resize the texture only if required. + if(data_type_size != write_area_data_size()) { + write_area_texture_.resize(WriteAreaWidth*WriteAreaHeight*data_type_size); + set_write_area(write_area_texture_.data()); } // Prepare to bind line shaders. @@ -400,17 +160,13 @@ void ScanTarget::setup_pipeline() { input_shader_->set_uniform("textureName", GLint(SourceDataTextureUnit - GL_TEXTURE0)); } -Outputs::Display::Metrics &ScanTarget::display_metrics() { - return display_metrics_; -} - bool ScanTarget::is_soft_display_type() { return modals_.display_type == DisplayType::CompositeColour || modals_.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 the GPU is still busy, don't wait; we'll catch it next time if(glClientWaitSync(fence_, GL_SYNC_FLUSH_COMMANDS_BIT, 0) == GL_TIMEOUT_EXPIRED) { display_metrics_.announce_draw_status( lines_submitted_, @@ -420,322 +176,311 @@ void ScanTarget::update(int, int output_height) { } fence_ = nullptr; } + + // Update the display metrics. display_metrics_.announce_draw_status( lines_submitted_, std::chrono::high_resolution_clock::now() - line_submission_begin_time_, true); - // Spin until the is-drawing flag is reset; the wait sync above will deal - // with instances where waiting is inappropriate. - while(is_updating_.test_and_set()); - - // Establish the pipeline if necessary. - const bool did_setup_pipeline = modals_are_dirty_; - if(modals_are_dirty_) { - setup_pipeline(); - modals_are_dirty_ = false; - } - - // Determine the start time of this submission group. - line_submission_begin_time_ = std::chrono::high_resolution_clock::now(); - - // Grab the current read and submit pointers. - const auto submit_pointers = submit_pointers_.load(); - const auto read_pointers = read_pointers_.load(); - - // Determine how many lines are about to be submitted. - lines_submitted_ = (read_pointers.line + line_buffer_.size() - submit_pointers.line) % line_buffer_.size(); - - // Submit scans; only the new ones need to be communicated. - size_t new_scans = (submit_pointers.scan_buffer + scan_buffer_.size() - read_pointers.scan_buffer) % scan_buffer_.size(); - if(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); - uint8_t *const destination = static_cast( - glMapBufferRange(GL_ARRAY_BUFFER, 0, GLsizeiptr(new_scans_size), GL_MAP_WRITE_BIT | GL_MAP_FLUSH_EXPLICIT_BIT) - ); - test_gl_error(); - - if(read_pointers.scan_buffer < submit_pointers.scan_buffer) { - memcpy(destination, &scan_buffer_[read_pointers.scan_buffer], new_scans_size); - } else { - const size_t first_portion_length = (scan_buffer_.size() - read_pointers.scan_buffer) * sizeof(Scan); - memcpy(destination, &scan_buffer_[read_pointers.scan_buffer], first_portion_length); - memcpy(&destination[first_portion_length], &scan_buffer_[0], new_scans_size - first_portion_length); + // Grab the new output list. + perform([=] (const OutputArea &area) { + // Establish the pipeline if necessary. + const bool did_setup_pipeline = modals_are_dirty_; + if(modals_are_dirty_) { + setup_pipeline(); + modals_are_dirty_ = false; } - // Flush and unmap the buffer. - test_gl(glFlushMappedBufferRange, GL_ARRAY_BUFFER, 0, GLsizeiptr(new_scans_size)); - test_gl(glUnmapBuffer, GL_ARRAY_BUFFER); - } + // 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(submit_pointers.write_area != read_pointers.write_area) { - test_gl(glActiveTexture, SourceDataTextureUnit); - test_gl(glBindTexture, GL_TEXTURE_2D, write_area_texture_name_); + // Submit scans; only the new ones need to be communicated. + size_t new_scans = (area.end.scan - area.start.scan + scan_buffer_.size()) % scan_buffer_.size(); + if(new_scans) { + test_gl(glBindBuffer, GL_ARRAY_BUFFER, scan_buffer_name_); - // Create storage for the texture if it doesn't yet exist; this was deferred until here - // because the pixel format wasn't initially known. - if(!texture_exists_) { - test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - test_gl(glTexImage2D, - GL_TEXTURE_2D, - 0, - internalFormatForDepth(data_type_size_), - WriteAreaWidth, - WriteAreaHeight, - 0, - formatForDepth(data_type_size_), - GL_UNSIGNED_BYTE, - nullptr); - texture_exists_ = true; - } + // Map only the required portion of the buffer. + const size_t new_scans_size = new_scans * sizeof(Scan); + uint8_t *const destination = static_cast( + glMapBufferRange(GL_ARRAY_BUFFER, 0, GLsizeiptr(new_scans_size), GL_MAP_WRITE_BIT | GL_MAP_FLUSH_EXPLICIT_BIT) + ); + test_gl_error(); - const auto start_y = TextureAddressGetY(read_pointers.write_area); - const auto end_y = TextureAddressGetY(submit_pointers.write_area); - if(end_y >= start_y) { - // Submit the direct region from the submit pointer to the read pointer. - test_gl(glTexSubImage2D, - GL_TEXTURE_2D, 0, - 0, start_y, - WriteAreaWidth, - 1 + end_y - start_y, - formatForDepth(data_type_size_), - GL_UNSIGNED_BYTE, - &write_area_texture_[size_t(TextureAddress(0, start_y)) * data_type_size_]); - } 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. - test_gl(glTexSubImage2D, - GL_TEXTURE_2D, 0, - 0, 0, - WriteAreaWidth, - 1 + end_y, - formatForDepth(data_type_size_), - GL_UNSIGNED_BYTE, - &write_area_texture_[0]); - test_gl(glTexSubImage2D, - GL_TEXTURE_2D, 0, - 0, start_y, - WriteAreaWidth, - WriteAreaHeight - start_y, - formatForDepth(data_type_size_), - GL_UNSIGNED_BYTE, - &write_area_texture_[size_t(TextureAddress(0, start_y)) * data_type_size_]); - } - } - - // Push new input to the unprocessed line buffer. - if(new_scans) { - unprocessed_line_texture_.bind_framebuffer(); - - // Clear newly-touched lines; that is everything from (read+1) to submit. - const uint16_t first_line_to_clear = (read_pointers.line+1)%line_buffer_.size(); - const uint16_t final_line_to_clear = submit_pointers.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); + // Copy as a single chunk if possible; otherwise copy in two parts. + if(area.start.scan < area.end.scan) { + memcpy(destination, &scan_buffer_[size_t(area.start.scan)], new_scans_size); } else { - test_gl(glClearColor, 0.0f, 0.0f, 0.0f, 0.0f); + const size_t first_portion_length = (scan_buffer_.size() - area.start.scan) * sizeof(Scan); + memcpy(destination, &scan_buffer_[area.start.scan], first_portion_length); + memcpy(&destination[first_portion_length], &scan_buffer_[0], new_scans_size - first_portion_length); } - if(first_line_to_clear < final_line_to_clear) { - test_gl(glScissor, 0, first_line_to_clear, unprocessed_line_texture_.get_width(), final_line_to_clear - first_line_to_clear); - test_gl(glClear, GL_COLOR_BUFFER_BIT); + // Flush and unmap the buffer. + test_gl(glFlushMappedBufferRange, GL_ARRAY_BUFFER, 0, GLsizeiptr(new_scans_size)); + test_gl(glUnmapBuffer, GL_ARRAY_BUFFER); + } + + // Submit texture. + if(area.start.write_area_x != area.end.write_area_x || area.start.write_area_y != area.end.write_area_y) { + test_gl(glActiveTexture, SourceDataTextureUnit); + test_gl(glBindTexture, GL_TEXTURE_2D, write_area_texture_name_); + + // Create storage for the texture if it doesn't yet exist; this was deferred until here + // because the pixel format wasn't initially known. + if(!texture_exists_) { + test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + test_gl(glTexParameteri, GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + test_gl(glTexImage2D, + GL_TEXTURE_2D, + 0, + internalFormatForDepth(write_area_data_size()), + WriteAreaWidth, + WriteAreaHeight, + 0, + formatForDepth(write_area_data_size()), + GL_UNSIGNED_BYTE, + nullptr); + texture_exists_ = true; + } + + if(area.end.write_area_y >= area.start.write_area_y) { + // Submit the direct region from the submit pointer to the read pointer. + test_gl(glTexSubImage2D, + GL_TEXTURE_2D, 0, + 0, area.start.write_area_y, + WriteAreaWidth, + 1 + area.end.write_area_y - area.start.write_area_y, + formatForDepth(write_area_data_size()), + GL_UNSIGNED_BYTE, + &write_area_texture_[size_t(area.start.write_area_y * WriteAreaWidth) * write_area_data_size()]); } else { - test_gl(glScissor, 0, 0, unprocessed_line_texture_.get_width(), final_line_to_clear); - test_gl(glClear, GL_COLOR_BUFFER_BIT); - test_gl(glScissor, 0, first_line_to_clear, unprocessed_line_texture_.get_width(), unprocessed_line_texture_.get_height() - first_line_to_clear); - test_gl(glClear, GL_COLOR_BUFFER_BIT); + // 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. + test_gl(glTexSubImage2D, + GL_TEXTURE_2D, 0, + 0, area.start.write_area_y, + WriteAreaWidth, + WriteAreaHeight - area.start.write_area_y, + formatForDepth(write_area_data_size()), + GL_UNSIGNED_BYTE, + &write_area_texture_[size_t(area.start.write_area_y * WriteAreaWidth) * write_area_data_size()]); + test_gl(glTexSubImage2D, + GL_TEXTURE_2D, 0, + 0, 0, + WriteAreaWidth, + 1 + area.end.write_area_y, + formatForDepth(write_area_data_size()), + GL_UNSIGNED_BYTE, + &write_area_texture_[0]); } - - 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)); - } + // Push new input to the unprocessed line buffer. + if(new_scans) { + unprocessed_line_texture_.bind_framebuffer(); - // 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; - } + // 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); - // 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_->get_width() != proportional_width || accumulation_texture_->get_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) { - LOG("Changed output resolution to " << proportional_width << " by " << framebuffer_height); - display_metrics_.announce_did_resize(); - std::unique_ptr new_framebuffer( - new TextureTarget( - 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. - uint16_t new_lines = (submit_pointers.line + LineBufferHeight - read_pointers.line) % 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_[read_pointers.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. - uint16_t start_line = read_pointers.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 != submit_pointers.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); + // 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); } - stencil_is_valid_ = true; + + if(first_line_to_clear < final_line_to_clear) { + test_gl(glScissor, GLint(0), GLint(first_line_to_clear), unprocessed_line_texture_.get_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_.get_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_.get_width(), unprocessed_line_texture_.get_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)); + } + + // 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_->get_width() != proportional_width || accumulation_texture_->get_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) { + LOG("Changed output resolution to " << proportional_width << " by " << framebuffer_height); + display_metrics_.announce_did_resize(); + std::unique_ptr new_framebuffer( + new TextureTarget( + 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); - // Rebind the program for span output. - test_gl(glBindVertexArray, line_vertex_array_); - if(!qam_separation_shader_) { - output_shader_->bind(); - } + new_framebuffer->bind_texture(); } + accumulation_texture_ = std::move(new_framebuffer); - // 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 { - uint8_t *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(); + // 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; + } - const size_t buffer_length = line_buffer_.size() * sizeof(Line); - const size_t start_position = start_line * sizeof(Line); - memcpy(&destination[0], &line_buffer_[start_line], buffer_length - start_position); - memcpy(&destination[buffer_length - start_position], &line_buffer_[0], end_line * sizeof(Line)); + if(did_setup_pipeline || did_create_accumulation_texture) { + set_sampling_window(proportional_width, framebuffer_height, *output_shader_); + } - 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)); + // 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); } - // Render to the output. - test_gl(glDrawArraysInstanced, GL_TRIANGLE_STRIP, 0, 4, GLsizei(lines)); + // Set the proper stencil function regardless. + test_gl(glStencilFunc, GL_EQUAL, 0, GLuint(~0)); + test_gl(glStencilOp, GL_KEEP, GL_KEEP, GL_INCR); - start_line = end_line; - new_lines -= lines; + // 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 { + uint8_t *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(); + + const size_t buffer_length = line_buffer_.size() * sizeof(Line); + const size_t start_position = start_line * sizeof(Line); + memcpy(&destination[0], &line_buffer_[start_line], buffer_length - start_position); + memcpy(&destination[buffer_length - start_position], &line_buffer_[0], end_line * sizeof(Line)); + + 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); } - // 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(); - // That's it for operations affecting the accumulation buffer. - is_drawing_to_accumulation_buffer_.clear(); - - // All data now having been spooled to the GPU, update the read pointers to - // the submit pointer location. - read_pointers_.store(submit_pointers); - - // 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); - is_updating_.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); + }); } void ScanTarget::draw(int output_width, int output_height) { - while(is_drawing_to_accumulation_buffer_.test_and_set()); + while(is_drawing_to_accumulation_buffer_.test_and_set(std::memory_order_acquire)); if(accumulation_texture_) { // Copy the accumulation texture to the target. @@ -748,5 +493,5 @@ void ScanTarget::draw(int output_width, int output_height) { accumulation_texture_->draw(float(output_width) / float(output_height), 4.0f / 255.0f); } - is_drawing_to_accumulation_buffer_.clear(); + is_drawing_to_accumulation_buffer_.clear(std::memory_order_release); } diff --git a/Outputs/OpenGL/ScanTarget.hpp b/Outputs/OpenGL/ScanTarget.hpp index 56b29a7cb..5d8ee0437 100644 --- a/Outputs/OpenGL/ScanTarget.hpp +++ b/Outputs/OpenGL/ScanTarget.hpp @@ -11,7 +11,7 @@ #include "../Log.hpp" #include "../DisplayMetrics.hpp" -#include "../ScanTarget.hpp" +#include "../ScanTargets/BufferingScanTarget.hpp" #include "OpenGL.hpp" #include "Primitives/TextureTarget.hpp" @@ -32,12 +32,13 @@ namespace Outputs { namespace Display { namespace OpenGL { + /*! Provides a ScanTarget that uses OpenGL to render its output; this uses various internal buffers so that the only geometry drawn to the target framebuffer is a quad. */ -class ScanTarget: public Outputs::Display::ScanTarget { +class ScanTarget: public Outputs::Display::BufferingScanTarget { public: ScanTarget(GLuint target_framebuffer = 0, float output_gamma = 2.2f); ~ScanTarget(); @@ -49,10 +50,10 @@ class ScanTarget: public Outputs::Display::ScanTarget { /*! Processes all the latest input, at a resolution suitable for later output to a framebuffer of the specified size. */ void update(int output_width, int output_height); - /*! @returns The DisplayMetrics object that this ScanTarget has been providing with announcements and draw overages. */ - Metrics &display_metrics(); - private: + static constexpr int LineBufferWidth = 2048; + static constexpr int LineBufferHeight = 2048; + #ifndef NDEBUG struct OpenGLVersionDumper { OpenGLVersionDumper() { @@ -62,93 +63,15 @@ class ScanTarget: public Outputs::Display::ScanTarget { } dumper_; #endif - static constexpr int WriteAreaWidth = 2048; - static constexpr int WriteAreaHeight = 2048; - - static constexpr int LineBufferWidth = 2048; - static constexpr int LineBufferHeight = 2048; - GLuint target_framebuffer_; const float output_gamma_; - // Outputs::Display::ScanTarget finals. - void set_modals(Modals) final; - Scan *begin_scan() final; - void end_scan() final; - uint8_t *begin_data(size_t required_length, size_t required_alignment) final; - void end_data(size_t actual_length) final; - void announce(Event event, bool is_visible, const Outputs::Display::ScanTarget::Scan::EndPoint &location, uint8_t colour_burst_amplitude) final; - void will_change_owner() final; - - bool output_is_visible_ = false; - - Metrics display_metrics_; int resolution_reduction_level_ = 1; int output_height_ = 0; size_t lines_submitted_ = 0; std::chrono::high_resolution_clock::time_point line_submission_begin_time_; - // Extends the definition of a Scan to include two extra fields, - // relevant to the way that this scan target processes video. - struct Scan { - Outputs::Display::ScanTarget::Scan scan; - - /// Stores the y coordinate that this scan's data is at, within the write area texture. - uint16_t data_y; - /// Stores the y coordinate of this scan within the line buffer. - uint16_t line; - }; - - struct PointerSet { - // This constructor is here to appease GCC's interpretation of - // an ambiguity in the C++ standard; cf. https://stackoverflow.com/questions/17430377 - PointerSet() noexcept {} - - // The sizes below might be less hassle as something more natural like ints, - // but squeezing this struct into 64 bits makes the std::atomics more likely - // to be lock free; they are under LLVM x86-64. - int write_area = 1; // By convention this points to the vended area. Which is preceded by a guard pixel. So a sensible default construction is write_area = 1. - uint16_t scan_buffer = 0; - uint16_t line = 0; - }; - - /// A pointer to the next thing that should be provided to the caller for data. - PointerSet write_pointers_; - - /// A mutex for gettng access to write_pointers_; access to write_pointers_, - /// data_type_size_ or write_area_texture_ is almost never contended, so this - /// is cheap for the main use case. - std::mutex write_pointers_mutex_; - - /// A pointer to the final thing currently cleared for submission. - std::atomic submit_pointers_; - - /// A pointer to the first thing not yet submitted for display. - std::atomic read_pointers_; - - /// Maintains a buffer of the most recent scans. - std::array scan_buffer_; - - // Maintains a list of composite scan buffer coordinates; the Line struct - // is transported to the GPU in its entirety; the LineMetadatas live in CPU - // space only. - struct Line { - struct EndPoint { - uint16_t x, y; - uint16_t cycles_since_end_of_horizontal_retrace; - int16_t composite_angle; - } end_points[2]; - uint16_t line; - uint8_t composite_amplitude; - }; - struct LineMetadata { - bool is_first_in_frame; - bool previous_frame_was_complete; - }; - std::array line_buffer_; - std::array line_metadata_buffer_; - // Contains the first composition of scans into lines; // they're accumulated prior to output to allow for continuous // application of any necessary conversions — e.g. composite processing. @@ -164,13 +87,6 @@ class ScanTarget: public Outputs::Display::ScanTarget { Rectangle full_display_rectangle_; bool stencil_is_valid_ = false; - // Ephemeral state that helps in line composition. - Line *active_line_ = nullptr; - int provided_scans_ = 0; - bool is_first_in_frame_ = true; - bool frame_is_complete_ = true; - bool previous_frame_was_complete_ = true; - // OpenGL storage handles for buffer data. GLuint scan_buffer_name_ = 0, scan_vertex_array_ = 0; GLuint line_buffer_name_ = 0, line_vertex_array_ = 0; @@ -178,24 +94,10 @@ class ScanTarget: public Outputs::Display::ScanTarget { template void allocate_buffer(const T &array, GLuint &buffer_name, GLuint &vertex_array_name); template void patch_buffer(const T &array, GLuint target, uint16_t submit_pointer, uint16_t read_pointer); - // Uses a texture to vend write areas. - std::vector write_area_texture_; - size_t data_type_size_ = 0; - GLuint write_area_texture_name_ = 0; bool texture_exists_ = false; - // Ephemeral information for the begin/end functions. - Scan *vended_scan_ = nullptr; - int vended_write_area_pointer_ = 0; - - // Track allocation failures. - bool data_is_allocated_ = false; - bool allocation_has_failed_ = false; - // Receives scan target modals. - Modals modals_; - bool modals_are_dirty_ = false; void setup_pipeline(); enum class ShaderType { @@ -213,14 +115,12 @@ class ScanTarget: public Outputs::Display::ScanTarget { std::vector bindings(ShaderType type) const; GLsync fence_ = nullptr; - std::atomic_flag is_updating_; std::atomic_flag is_drawing_to_accumulation_buffer_; std::unique_ptr input_shader_; std::unique_ptr output_shader_; std::unique_ptr qam_separation_shader_; - /*! Produces a shader that composes fragment of the input stream to a single buffer, normalising the data into one of four forms: RGB, 8-bit luminance, @@ -248,6 +148,12 @@ class ScanTarget: public Outputs::Display::ScanTarget { contrast tends to be low, such as a composite colour display. */ bool is_soft_display_type(); + + // Storage for the various buffers. + std::vector write_area_texture_; + std::array scan_buffer_; + std::array line_buffer_; + std::array line_metadata_buffer_; }; } diff --git a/Outputs/ScanTargets/BufferingScanTarget.cpp b/Outputs/ScanTargets/BufferingScanTarget.cpp new file mode 100644 index 000000000..e8344653a --- /dev/null +++ b/Outputs/ScanTargets/BufferingScanTarget.cpp @@ -0,0 +1,312 @@ +// +// BufferingScanTarget.cpp +// Clock Signal +// +// Created by Thomas Harte on 22/07/2020. +// Copyright © 2020 Thomas Harte. All rights reserved. +// + +#include "BufferingScanTarget.hpp" + +#include +#include + +#define TextureAddressGetY(v) uint16_t((v) >> 11) +#define TextureAddressGetX(v) uint16_t((v) & 0x7ff) +#define TextureSub(a, b) (((a) - (b)) & 0x3fffff) +#define TextureAddress(x, y) (((y) << 11) | (x)) + +using namespace Outputs::Display; + +BufferingScanTarget::BufferingScanTarget() { + // Ensure proper initialisation of the two atomic pointer sets. + read_pointers_.store(write_pointers_); + submit_pointers_.store(write_pointers_); + + // Establish initial state for is_updating_. + is_updating_.clear(); +} + +void BufferingScanTarget::end_scan() { + if(vended_scan_) { + std::lock_guard lock_guard(write_pointers_mutex_); + vended_scan_->data_y = TextureAddressGetY(vended_write_area_pointer_); + vended_scan_->line = write_pointers_.line; + vended_scan_->scan.end_points[0].data_offset += TextureAddressGetX(vended_write_area_pointer_); + vended_scan_->scan.end_points[1].data_offset += TextureAddressGetX(vended_write_area_pointer_); + +#ifdef LOG_SCANS + if(vended_scan_->scan.composite_amplitude) { + std::cout << "S: "; + std::cout << vended_scan_->scan.end_points[0].composite_angle << "/" << vended_scan_->scan.end_points[0].data_offset << "/" << vended_scan_->scan.end_points[0].cycles_since_end_of_horizontal_retrace << " -> "; + std::cout << vended_scan_->scan.end_points[1].composite_angle << "/" << vended_scan_->scan.end_points[1].data_offset << "/" << vended_scan_->scan.end_points[1].cycles_since_end_of_horizontal_retrace << " => "; + std::cout << double(vended_scan_->scan.end_points[1].composite_angle - vended_scan_->scan.end_points[0].composite_angle) / (double(vended_scan_->scan.end_points[1].data_offset - vended_scan_->scan.end_points[0].data_offset) * 64.0f) << "/"; + std::cout << double(vended_scan_->scan.end_points[1].composite_angle - vended_scan_->scan.end_points[0].composite_angle) / (double(vended_scan_->scan.end_points[1].cycles_since_end_of_horizontal_retrace - vended_scan_->scan.end_points[0].cycles_since_end_of_horizontal_retrace) * 64.0f); + std::cout << std::endl; + } +#endif + } + vended_scan_ = nullptr; +} + +uint8_t *BufferingScanTarget::begin_data(size_t required_length, size_t required_alignment) { + assert(required_alignment); + + if(allocation_has_failed_) return nullptr; + + std::lock_guard lock_guard(write_pointers_mutex_); + if(!write_area_) { + allocation_has_failed_ = true; + return nullptr; + } + + // Determine where the proposed write area would start and end. + uint16_t output_y = TextureAddressGetY(write_pointers_.write_area); + + uint16_t aligned_start_x = TextureAddressGetX(write_pointers_.write_area & 0xffff) + 1; + aligned_start_x += uint16_t((required_alignment - aligned_start_x%required_alignment)%required_alignment); + + uint16_t end_x = aligned_start_x + uint16_t(1 + required_length); + + if(end_x > WriteAreaWidth) { + output_y = (output_y + 1) % WriteAreaHeight; + aligned_start_x = uint16_t(required_alignment); + end_x = aligned_start_x + uint16_t(1 + required_length); + } + + // Check whether that steps over the read pointer. + const auto end_address = TextureAddress(end_x, output_y); + const auto read_pointers = read_pointers_.load(); + + const auto end_distance = TextureSub(end_address, read_pointers.write_area); + const auto previous_distance = TextureSub(write_pointers_.write_area, read_pointers.write_area); + + // If allocating this would somehow make the write pointer back away from the read pointer, + // there must not be enough space left. + if(end_distance < previous_distance) { + allocation_has_failed_ = true; + return nullptr; + } + + // Everything checks out, note expectation of a future end_data and return the pointer. + data_is_allocated_ = true; + vended_write_area_pointer_ = write_pointers_.write_area = TextureAddress(aligned_start_x, output_y); + + assert(write_pointers_.write_area >= 1 && ((size_t(write_pointers_.write_area) + required_length + 1) * data_type_size_) <= WriteAreaWidth*WriteAreaHeight*data_type_size_); + return &write_area_[size_t(write_pointers_.write_area) * data_type_size_]; + + // Note state at exit: + // write_pointers_.write_area points to the first pixel the client is expected to draw to. +} + +void BufferingScanTarget::end_data(size_t actual_length) { + if(allocation_has_failed_ || !data_is_allocated_) return; + + std::lock_guard lock_guard(write_pointers_mutex_); + + // Bookend the start of the new data, to safeguard for precision errors in sampling. + memcpy( + &write_area_[size_t(write_pointers_.write_area - 1) * data_type_size_], + &write_area_[size_t(write_pointers_.write_area) * data_type_size_], + data_type_size_); + + // Advance to the end of the current run. + write_pointers_.write_area += actual_length + 1; + + // Also bookend the end. + memcpy( + &write_area_[size_t(write_pointers_.write_area - 1) * data_type_size_], + &write_area_[size_t(write_pointers_.write_area - 2) * data_type_size_], + data_type_size_); + + // The write area was allocated in the knowledge that there's sufficient + // distance left on the current line, but there's a risk of exactly filling + // the final line, in which case this should wrap back to 0. + write_pointers_.write_area %= WriteAreaWidth*WriteAreaHeight; + + // Record that no further end_data calls are expected. + data_is_allocated_ = false; +} + +void BufferingScanTarget::will_change_owner() { + allocation_has_failed_ = true; + vended_scan_ = nullptr; +} + +void BufferingScanTarget::announce(Event event, bool is_visible, const Outputs::Display::ScanTarget::Scan::EndPoint &location, uint8_t composite_amplitude) { + // Forward the event to the display metrics tracker. + display_metrics_.announce_event(event); + + if(event == ScanTarget::Event::EndVerticalRetrace) { + // The previous-frame-is-complete flag is subject to a two-slot queue because + // measurement for *this* frame needs to begin now, meaning that the previous + // result needs to be put somewhere — it'll be attached to the first successful + // line output. + is_first_in_frame_ = true; + previous_frame_was_complete_ = frame_is_complete_; + frame_is_complete_ = true; + } + + if(output_is_visible_ == is_visible) return; + if(is_visible) { + const auto read_pointers = read_pointers_.load(); + std::lock_guard lock_guard(write_pointers_mutex_); + + // Commit the most recent line only if any scans fell on it. + // Otherwise there's no point outputting it, it'll contribute nothing. + if(provided_scans_) { + // Store metadata if concluding a previous line. + if(active_line_) { + line_metadata_buffer_[size_t(write_pointers_.line)].is_first_in_frame = is_first_in_frame_; + line_metadata_buffer_[size_t(write_pointers_.line)].previous_frame_was_complete = previous_frame_was_complete_; + is_first_in_frame_ = false; + } + + // Attempt to allocate a new line; note allocation failure if necessary. + const auto next_line = uint16_t((write_pointers_.line + 1) % line_buffer_size_); + if(next_line == read_pointers.line) { + allocation_has_failed_ = true; + active_line_ = nullptr; + } else { + write_pointers_.line = next_line; + active_line_ = &line_buffer_[size_t(write_pointers_.line)]; + } + provided_scans_ = 0; + } + + if(active_line_) { + active_line_->end_points[0].x = location.x; + active_line_->end_points[0].y = location.y; + active_line_->end_points[0].cycles_since_end_of_horizontal_retrace = location.cycles_since_end_of_horizontal_retrace; + active_line_->end_points[0].composite_angle = location.composite_angle; + active_line_->line = write_pointers_.line; + active_line_->composite_amplitude = composite_amplitude; + } + } else { + if(active_line_) { + // A successfully-allocated line is ending. + active_line_->end_points[1].x = location.x; + active_line_->end_points[1].y = location.y; + active_line_->end_points[1].cycles_since_end_of_horizontal_retrace = location.cycles_since_end_of_horizontal_retrace; + active_line_->end_points[1].composite_angle = location.composite_angle; + +#ifdef LOG_LINES + if(active_line_->composite_amplitude) { + std::cout << "L: "; + std::cout << active_line_->end_points[0].composite_angle << "/" << active_line_->end_points[0].cycles_since_end_of_horizontal_retrace << " -> "; + std::cout << active_line_->end_points[1].composite_angle << "/" << active_line_->end_points[1].cycles_since_end_of_horizontal_retrace << " => "; + std::cout << (active_line_->end_points[1].composite_angle - active_line_->end_points[0].composite_angle) << "/" << (active_line_->end_points[1].cycles_since_end_of_horizontal_retrace - active_line_->end_points[0].cycles_since_end_of_horizontal_retrace) << " => "; + std::cout << double(active_line_->end_points[1].composite_angle - active_line_->end_points[0].composite_angle) / (double(active_line_->end_points[1].cycles_since_end_of_horizontal_retrace - active_line_->end_points[0].cycles_since_end_of_horizontal_retrace) * 64.0f); + std::cout << std::endl; + } +#endif + } + + // A line is complete; submit latest updates if nothing failed. + if(allocation_has_failed_) { + // Reset all pointers to where they were; this also means + // the stencil won't be properly populated. + write_pointers_ = submit_pointers_.load(); + frame_is_complete_ = false; + } else { + // Advance submit pointer. + submit_pointers_.store(write_pointers_); + } + allocation_has_failed_ = false; + } + output_is_visible_ = is_visible; +} + +const Outputs::Display::Metrics &BufferingScanTarget::display_metrics() { + return display_metrics_; +} + +Outputs::Display::ScanTarget::Scan *BufferingScanTarget::begin_scan() { + if(allocation_has_failed_) return nullptr; + + std::lock_guard lock_guard(write_pointers_mutex_); + + const auto result = &scan_buffer_[write_pointers_.scan_buffer]; + const auto read_pointers = read_pointers_.load(); + + // Advance the pointer. + const auto next_write_pointer = decltype(write_pointers_.scan_buffer)((write_pointers_.scan_buffer + 1) % scan_buffer_size_); + + // Check whether that's too many. + if(next_write_pointer == read_pointers.scan_buffer) { + allocation_has_failed_ = true; + return nullptr; + } + write_pointers_.scan_buffer = next_write_pointer; + ++provided_scans_; + + // Fill in extra OpenGL-specific details. + result->line = write_pointers_.line; + + vended_scan_ = result; + return &result->scan; +} + +void BufferingScanTarget::set_write_area(uint8_t *base) { + std::lock_guard lock_guard(write_pointers_mutex_); + write_area_ = base; + data_type_size_ = Outputs::Display::size_for_data_type(modals_.input_data_type); + write_pointers_ = submit_pointers_ = read_pointers_ = PointerSet(); +} + +size_t BufferingScanTarget::write_area_data_size() const { + return data_type_size_; +} + +void BufferingScanTarget::set_modals(Modals modals) { + perform([=] { + modals_ = modals; + modals_are_dirty_ = true; + }); +} + +void BufferingScanTarget::perform(const std::function &function) { + // The area to draw is that between the read pointers, representing wherever reading + // last stopped, and the submit pointers, representing all the new data that has been + // cleared for submission. + const auto submit_pointers = submit_pointers_.load(); + const auto read_pointers = read_pointers_.load(); + + OutputArea area; + + area.start.line = read_pointers.line; + area.end.line = submit_pointers.line; + + area.start.scan = read_pointers.scan_buffer; + area.end.scan = submit_pointers.scan_buffer; + + area.start.write_area_x = TextureAddressGetX(read_pointers.write_area); + area.start.write_area_y = TextureAddressGetY(read_pointers.write_area); + area.end.write_area_x = TextureAddressGetX(submit_pointers.write_area); + area.end.write_area_y = TextureAddressGetY(submit_pointers.write_area); + + // Perform only while holding the is_updating lock. + while(is_updating_.test_and_set(std::memory_order_acquire)); + function(area); + is_updating_.clear(std::memory_order_release); + + // Update the read pointers. + read_pointers_.store(submit_pointers); +} + +void BufferingScanTarget::perform(const std::function &function) { + while(is_updating_.test_and_set(std::memory_order_acquire)); + function(); + is_updating_.clear(std::memory_order_release); +} + +void BufferingScanTarget::set_scan_buffer(Scan *buffer, size_t size) { + scan_buffer_ = buffer; + scan_buffer_size_ = size; +} + +void BufferingScanTarget::set_line_buffer(Line *line_buffer, LineMetadata *metadata_buffer, size_t size) { + line_buffer_ = line_buffer; + line_metadata_buffer_ = metadata_buffer; + line_buffer_size_ = size; +} diff --git a/Outputs/ScanTargets/BufferingScanTarget.hpp b/Outputs/ScanTargets/BufferingScanTarget.hpp new file mode 100644 index 000000000..d5897faf7 --- /dev/null +++ b/Outputs/ScanTargets/BufferingScanTarget.hpp @@ -0,0 +1,222 @@ +// +// BufferingScanTarget.hpp +// Clock Signal +// +// Created by Thomas Harte on 22/07/2020. +// Copyright © 2020 Thomas Harte. All rights reserved. +// + +#ifndef BufferingScanTarget_hpp +#define BufferingScanTarget_hpp + +#include "../ScanTarget.hpp" +#include "../DisplayMetrics.hpp" + +#include +#include +#include +#include +#include + +namespace Outputs { +namespace Display { + +/*! + Provides basic thread-safe (hopefully) circular queues for any scan target that: + + * will store incoming Scans into a linear circular buffer and pack regions of + incoming pixel data into a 2048x2048 2d texture; + * will compose whole lines of content by partioning the Scans based on sync + placement and then pasting together their content; + * will process those lines as necessary to map from input format to whatever + suits the display; and + * will then output the lines. + + This buffer rejects new data when full. +*/ +class BufferingScanTarget: public Outputs::Display::ScanTarget { + public: + /*! @returns The DisplayMetrics object that this ScanTarget has been providing with announcements and draw overages. */ + const Metrics &display_metrics(); + + protected: + static constexpr int WriteAreaWidth = 2048; + static constexpr int WriteAreaHeight = 2048; + + BufferingScanTarget(); + + // This is included because it's assumed that scan targets will want to expose one. + // It is the subclass's responsibility to post timings. + Metrics display_metrics_; + + // Extends the definition of a Scan to include two extra fields, + // completing this scan's source data and destination locations. + struct Scan { + Outputs::Display::ScanTarget::Scan scan; + + /// Stores the y coordinate for this scan's data within the write area texture. + /// Use this plus the scan's endpoints' data_offsets to locate this data in 2d. + uint16_t data_y; + /// Stores the y coordinate assigned to this scan within the intermediate buffers. + /// Use this plus this scan's endpoints' x locations to determine where to composite + /// this data for intermediate processing. + uint16_t line; + }; + + /// Defines the boundaries of a complete line of video — a 2d start and end location, + /// composite phase and amplitude (if relevant), the source line in the intermediate buffer + /// plus the start and end offsets of the area that is visible from the intermediate buffer. + struct Line { + struct EndPoint { + uint16_t x, y; + uint16_t cycles_since_end_of_horizontal_retrace; + int16_t composite_angle; + } end_points[2]; + uint16_t line; + uint8_t composite_amplitude; + }; + + /// Provides additional metadata about lines; this is separate because it's unlikely to be of + /// interest to the GPU, unlike the fields in Line. + struct LineMetadata { + /// @c true if this line was the first drawn after vertical sync; @c false otherwise. + bool is_first_in_frame; + /// @c true if this line is the first in the frame and if every single piece of output + /// from the previous frame was recorded; @c false otherwise. Data can be dropped + /// from a frame if performance problems mean that the emulated machine is running + /// more quickly than complete frames can be generated. + bool previous_frame_was_complete; + }; + + /// Sets the area of memory to use as a scan buffer. + void set_scan_buffer(Scan *buffer, size_t size); + + /// Sets the area of memory to use as line and line metadata buffers. + void set_line_buffer(Line *line_buffer, LineMetadata *metadata_buffer, size_t size); + + // These are safe to read only within a `perform` block. + // TODO: can I do better than that? + Modals modals_; + bool modals_are_dirty_ = false; + + /// Sets a new base address for the texture. + /// When called this will flush all existing data and load up the + /// new data size. + void set_write_area(uint8_t *base); + + /// @returns The number of bytes per input sample, as per the latest modals. + size_t write_area_data_size() const; + + /// Defines a segment of data now ready for output, consisting of start and endpoints for: + /// + /// (i) the region of the write area that has been modified; if the caller is using shared memory + /// for the write area then it can ignore this information; + /// + /// (ii) the number of scans that have been completed; and + /// + /// (iii) the number of lines that have been completed. + /// + /// New write areas and scans are exposed only upon completion of the corresponding lines. + struct OutputArea { + struct Endpoint { + int write_area_x, write_area_y; + size_t scan; + size_t line; + }; + + Endpoint start, end; + }; + /// Performs @c action ensuring that no other @c perform actions, or any + /// change to modals, occurs simultaneously. + void perform(const std::function &action); + + /// Acts as per void(void) @c perform but also dequeues all latest available video output. + void perform(const std::function &); + + private: + // ScanTarget overrides. + void set_modals(Modals) final; + Outputs::Display::ScanTarget::Scan *begin_scan() final; + void end_scan() final; + uint8_t *begin_data(size_t required_length, size_t required_alignment) final; + void end_data(size_t actual_length) final; + void announce(Event event, bool is_visible, const Outputs::Display::ScanTarget::Scan::EndPoint &location, uint8_t colour_burst_amplitude) final; + void will_change_owner() final; + + // Uses a texture to vend write areas. + uint8_t *write_area_ = nullptr; + size_t data_type_size_ = 0; + + // Tracks changes in raster visibility in order to populate + // Lines and LineMetadatas. + bool output_is_visible_ = false; + + // Track allocation failures. + bool data_is_allocated_ = false; + bool allocation_has_failed_ = false; + + // Ephemeral information for the begin/end functions. + Scan *vended_scan_ = nullptr; + int vended_write_area_pointer_ = 0; + + // Ephemeral state that helps in line composition. + Line *active_line_ = nullptr; + int provided_scans_ = 0; + bool is_first_in_frame_ = true; + bool frame_is_complete_ = true; + bool previous_frame_was_complete_ = true; + + // TODO: make this an implementation detail. + // ... and expose some sort of difference? + struct PointerSet { + // This constructor is here to appease GCC's interpretation of + // an ambiguity in the C++ standard; cf. https://stackoverflow.com/questions/17430377 + PointerSet() noexcept {} + + // Squeezing this struct into 64 bits makes the std::atomics more likely + // to be lock free; they are under LLVM x86-64. + + // Points to the vended area in the write area texture. + // The vended area is always preceded by a guard pixel, so a + // sensible default construction is write_area = 1. + int32_t write_area = 1; + + // Points into the scan buffer. + uint16_t scan_buffer = 0; + + // Points into the line buffer. + uint16_t line = 0; + }; + + /// A pointer to the final thing currently cleared for submission. + std::atomic submit_pointers_; + + /// A pointer to the first thing not yet submitted for display. + std::atomic read_pointers_; + + /// This is used as a spinlock to guard `perform` calls. + std::atomic_flag is_updating_; + + /// A mutex for gettng access to write_pointers_; access to write_pointers_, + /// data_type_size_ or write_area_texture_ is almost never contended, so this + /// is cheap for the main use case. + std::mutex write_pointers_mutex_; + + /// A pointer to the next thing that should be provided to the caller for data. + PointerSet write_pointers_; + + // The owner-supplied scan buffer and size. + Scan *scan_buffer_ = nullptr; + size_t scan_buffer_size_ = 0; + + // The owner-supplied line buffer and size. + Line *line_buffer_ = nullptr; + LineMetadata *line_metadata_buffer_ = nullptr; + size_t line_buffer_size_ = 0; +}; + + +} +} + +#endif /* BufferingScanTarget_hpp */