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/Outputs/OpenGL/ScanTarget.cpp b/Outputs/OpenGL/ScanTarget.cpp index 9a4700ab4..dca2f59cf 100644 --- a/Outputs/OpenGL/ScanTarget.cpp +++ b/Outputs/OpenGL/ScanTarget.cpp @@ -40,11 +40,6 @@ constexpr GLenum QAMChromaTextureUnit = GL_TEXTURE2; /// The texture unit that contains the current display. constexpr GLenum AccumulationTextureUnit = GL_TEXTURE3; -#define TextureAddress(x, y) (((y) << 11) | (x)) -#define TextureAddressGetY(v) uint16_t((v) >> 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; @@ -119,231 +114,6 @@ void ScanTarget::set_target_framebuffer(GLuint target_framebuffer) { is_updating_.clear(); } -void BufferingScanTarget::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 *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::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_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 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_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 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) % 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; -} - void ScanTarget::setup_pipeline() { const auto data_type_size = Outputs::Display::size_for_data_type(modals_.input_data_type); @@ -400,10 +170,6 @@ void ScanTarget::setup_pipeline() { input_shader_->set_uniform("textureName", GLint(SourceDataTextureUnit - GL_TEXTURE0)); } -const Outputs::Display::Metrics &BufferingScanTarget::display_metrics() { - return display_metrics_; -} - bool ScanTarget::is_soft_display_type() { return modals_.display_type == DisplayType::CompositeColour || modals_.display_type == DisplayType::CompositeMonochrome; } diff --git a/Outputs/OpenGL/ScanTarget.hpp b/Outputs/OpenGL/ScanTarget.hpp index 4415d64c5..8db0c50b6 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,150 +32,13 @@ namespace Outputs { namespace Display { namespace OpenGL { -/*! - 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 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: - // 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; - }; - - // TODO: put this behind accessors. - std::atomic_flag is_updating_; - - // These are safe to read if you have is_updating_. - Modals modals_; - bool modals_are_dirty_ = false; - - // Track allocation failures. - bool data_is_allocated_ = false; - bool allocation_has_failed_ = false; - - /// Maintains a buffer of the most recent scans. - // TODO: have the owner supply a buffer and its size. - // That'll allow owners to place this in shared video memory if possible. - std::array scan_buffer_; - - /// 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_; - - 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. - 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 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_; - - // 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; - - // Ephemeral information for the begin/end functions. - Scan *vended_scan_ = nullptr; - int vended_write_area_pointer_ = 0; - - static constexpr int WriteAreaWidth = 2048; - static constexpr int WriteAreaHeight = 2048; - - static constexpr int LineBufferWidth = 2048; - static constexpr int LineBufferHeight = 2048; - - Metrics display_metrics_; - - // Uses a texture to vend write areas. - std::vector write_area_texture_; - size_t data_type_size_ = 0; - - bool output_is_visible_ = false; - - std::array line_buffer_; - std::array line_metadata_buffer_; - - 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; -}; /*! 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 BufferingScanTarget { +class ScanTarget: public Outputs::Display::BufferingScanTarget { public: ScanTarget(GLuint target_framebuffer = 0, float output_gamma = 2.2f); ~ScanTarget(); @@ -200,9 +63,6 @@ class ScanTarget: public BufferingScanTarget { GLuint target_framebuffer_; const float output_gamma_; - // Outputs::Display::ScanTarget finals. - void will_change_owner() final; - int resolution_reduction_level_ = 1; int output_height_ = 0; diff --git a/Outputs/ScanTargets/BufferingScanTarget.cpp b/Outputs/ScanTargets/BufferingScanTarget.cpp new file mode 100644 index 000000000..6cdbd2199 --- /dev/null +++ b/Outputs/ScanTargets/BufferingScanTarget.cpp @@ -0,0 +1,240 @@ +// +// BufferingScanTarget.cpp +// Clock Signal +// +// Created by Thomas Harte on 22/07/2020. +// Copyright © 2020 Thomas Harte. All rights reserved. +// + +#include "BufferingScanTarget.hpp" + +using namespace Outputs::Display; + +void BufferingScanTarget::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(); +} + +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_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 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_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 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) % 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; +} + +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; +} diff --git a/Outputs/ScanTargets/BufferingScanTarget.hpp b/Outputs/ScanTargets/BufferingScanTarget.hpp new file mode 100644 index 000000000..ffd0cedb7 --- /dev/null +++ b/Outputs/ScanTargets/BufferingScanTarget.hpp @@ -0,0 +1,171 @@ +// +// 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 + +#define TextureAddress(x, y) (((y) << 11) | (x)) +#define TextureAddressGetY(v) uint16_t((v) >> 11) +#define TextureAddressGetX(v) uint16_t((v) & 0x7ff) +#define TextureSub(a, b) (((a) - (b)) & 0x3fffff) + +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 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: + // 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; + }; + + // TODO: put this behind accessors. + std::atomic_flag is_updating_; + + // These are safe to read if you have is_updating_. + Modals modals_; + bool modals_are_dirty_ = false; + + // Track allocation failures. + bool data_is_allocated_ = false; + bool allocation_has_failed_ = false; + + /// Maintains a buffer of the most recent scans. + // TODO: have the owner supply a buffer and its size. + // That'll allow owners to place this in shared video memory if possible. + std::array scan_buffer_; + + /// 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_; + + 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. + 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 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_; + + // 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; + + // Ephemeral information for the begin/end functions. + Scan *vended_scan_ = nullptr; + int vended_write_area_pointer_ = 0; + + static constexpr int WriteAreaWidth = 2048; + static constexpr int WriteAreaHeight = 2048; + + static constexpr int LineBufferWidth = 2048; + static constexpr int LineBufferHeight = 2048; + + Metrics display_metrics_; + + // Uses a texture to vend write areas. + std::vector write_area_texture_; + size_t data_type_size_ = 0; + + bool output_is_visible_ = false; + + std::array line_buffer_; + std::array line_metadata_buffer_; + + 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; +}; + + +} +} + +#endif /* BufferingScanTarget_hpp */