mirror of
https://github.com/TomHarte/CLK.git
synced 2024-11-23 03:32:32 +00:00
Merge pull request #817 from TomHarte/LockFreeQueue
Fully splits buffering from drawing for the existing OpenGL scan target.
This commit is contained in:
commit
b14bedbe29
@ -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 = "<group>"; };
|
||||
4BC23A2A2467600E001A6030 /* EnvelopeGenerator.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = EnvelopeGenerator.hpp; sourceTree = "<group>"; };
|
||||
4BC23A2B2467600E001A6030 /* OPLL.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = OPLL.cpp; sourceTree = "<group>"; };
|
||||
4BC3C67A24C9230F0027BF76 /* BufferingScanTarget.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = BufferingScanTarget.cpp; path = ../../Outputs/ScanTargets/BufferingScanTarget.cpp; sourceTree = "<group>"; };
|
||||
4BC3C67B24C9230F0027BF76 /* BufferingScanTarget.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = BufferingScanTarget.hpp; path = ../../Outputs/ScanTargets/BufferingScanTarget.hpp; sourceTree = "<group>"; };
|
||||
4BC57CD2243427C700FBC404 /* AudioProducer.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = AudioProducer.hpp; sourceTree = "<group>"; };
|
||||
4BC57CD32434282000FBC404 /* TimedMachine.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = TimedMachine.hpp; sourceTree = "<group>"; };
|
||||
4BC57CD424342E0600FBC404 /* MachineTypes.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = MachineTypes.hpp; sourceTree = "<group>"; };
|
||||
@ -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 */,
|
||||
|
@ -67,7 +67,7 @@
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
buildConfiguration = "Release"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableASanStackUseAfterReturn = "YES"
|
||||
|
@ -85,6 +85,7 @@ SOURCES += \
|
||||
\
|
||||
$$SRC/Outputs/*.cpp \
|
||||
$$SRC/Outputs/CRT/*.cpp \
|
||||
$$SRC/Outputs/ScanTargets/*.cpp \
|
||||
$$SRC/Outputs/OpenGL/*.cpp \
|
||||
$$SRC/Outputs/OpenGL/Primitives/*.cpp \
|
||||
\
|
||||
@ -204,6 +205,7 @@ HEADERS += \
|
||||
$$SRC/Outputs/*.hpp \
|
||||
$$SRC/Outputs/CRT/*.hpp \
|
||||
$$SRC/Outputs/CRT/Internals/*.hpp \
|
||||
$$SRC/Outputs/ScanTargets/*.hpp \
|
||||
$$SRC/Outputs/OpenGL/*.hpp \
|
||||
$$SRC/Outputs/OpenGL/Primitives/*.hpp \
|
||||
$$SRC/Outputs/Speaker/*.hpp \
|
||||
|
@ -79,6 +79,7 @@ SOURCES += glob.glob('../../Machines/ZX8081/*.cpp')
|
||||
|
||||
SOURCES += glob.glob('../../Outputs/*.cpp')
|
||||
SOURCES += glob.glob('../../Outputs/CRT/*.cpp')
|
||||
SOURCES += glob.glob('../../Outputs/ScanTargets/*.cpp')
|
||||
SOURCES += glob.glob('../../Outputs/OpenGL/*.cpp')
|
||||
SOURCES += glob.glob('../../Outputs/OpenGL/Primitives/*.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;
|
||||
@ -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<uint8_t *>(
|
||||
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<uint8_t *>(
|
||||
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<OpenGL::TextureTarget> 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<OpenGL::TextureTarget> 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<uint8_t *>(
|
||||
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<uint8_t *>(
|
||||
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);
|
||||
}
|
||||
|
@ -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<PointerSet> submit_pointers_;
|
||||
|
||||
/// A pointer to the first thing not yet submitted for display.
|
||||
std::atomic<PointerSet> read_pointers_;
|
||||
|
||||
/// Maintains a buffer of the most recent scans.
|
||||
std::array<Scan, 16384> 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, LineBufferHeight> line_buffer_;
|
||||
std::array<LineMetadata, LineBufferHeight> 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 <typename T> void allocate_buffer(const T &array, GLuint &buffer_name, GLuint &vertex_array_name);
|
||||
template <typename T> 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<uint8_t> 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<std::string> bindings(ShaderType type) const;
|
||||
|
||||
GLsync fence_ = nullptr;
|
||||
std::atomic_flag is_updating_;
|
||||
std::atomic_flag is_drawing_to_accumulation_buffer_;
|
||||
|
||||
std::unique_ptr<Shader> input_shader_;
|
||||
std::unique_ptr<Shader> output_shader_;
|
||||
std::unique_ptr<Shader> 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<uint8_t> write_area_texture_;
|
||||
std::array<Scan, 16384> scan_buffer_;
|
||||
std::array<Line, LineBufferHeight> line_buffer_;
|
||||
std::array<LineMetadata, LineBufferHeight> line_metadata_buffer_;
|
||||
};
|
||||
|
||||
}
|
||||
|
312
Outputs/ScanTargets/BufferingScanTarget.cpp
Normal file
312
Outputs/ScanTargets/BufferingScanTarget.cpp
Normal file
@ -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 <cassert>
|
||||
#include <cstring>
|
||||
|
||||
#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<void(const OutputArea &)> &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<void(void)> &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;
|
||||
}
|
222
Outputs/ScanTargets/BufferingScanTarget.hpp
Normal file
222
Outputs/ScanTargets/BufferingScanTarget.hpp
Normal file
@ -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 <array>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
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<void(void)> &action);
|
||||
|
||||
/// Acts as per void(void) @c perform but also dequeues all latest available video output.
|
||||
void perform(const std::function<void(const OutputArea &)> &);
|
||||
|
||||
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<PointerSet> submit_pointers_;
|
||||
|
||||
/// A pointer to the first thing not yet submitted for display.
|
||||
std::atomic<PointerSet> 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 */
|
Loading…
Reference in New Issue
Block a user