diff options
Diffstat (limited to 'src/ui/widget/canvas/pixelstreamer.cpp')
-rw-r--r-- | src/ui/widget/canvas/pixelstreamer.cpp | 501 |
1 files changed, 501 insertions, 0 deletions
diff --git a/src/ui/widget/canvas/pixelstreamer.cpp b/src/ui/widget/canvas/pixelstreamer.cpp new file mode 100644 index 0000000..74d557b --- /dev/null +++ b/src/ui/widget/canvas/pixelstreamer.cpp @@ -0,0 +1,501 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include <cassert> +#include <cmath> +#include <vector> +#include <epoxy/gl.h> +#include "pixelstreamer.h" +#include "helper/mathfns.h" + +namespace Inkscape { +namespace UI { +namespace Widget { +namespace { + +cairo_user_data_key_t constexpr key{}; + +class PersistentPixelStreamer : public PixelStreamer +{ + static int constexpr bufsize = 0x1000000; // 16 MiB + + struct Buffer + { + GLuint pbo; // Pixel buffer object. + unsigned char *data; // The pointer to the mapped region. + int off; // Offset of the unused region, in bytes. Always a multiple of 64. + int refs; // How many mappings are currently using this buffer. + GLsync sync; // Sync object for telling us when the GPU has finished reading from this buffer. + bool ready; // Whether this buffer is ready for re-use. + + void create() + { + glGenBuffers(1, &pbo); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo); + glBufferStorage(GL_PIXEL_UNPACK_BUFFER, bufsize, nullptr, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT); + data = (unsigned char*)glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, bufsize, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | GL_MAP_FLUSH_EXPLICIT_BIT); + off = 0; + refs = 0; + } + + void destroy() + { + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo); + glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); + glDeleteBuffers(1, &pbo); + } + + // Advance a buffer in state 3 or 4 as far as possible towards state 5. + void advance() + { + if (!sync) { + sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + } else { + auto ret = glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, 0); + if (ret == GL_CONDITION_SATISFIED || ret == GL_ALREADY_SIGNALED) { + glDeleteSync(sync); + ready = true; + } + } + } + }; + std::vector<Buffer> buffers; + + int current_buffer; + + struct Mapping + { + bool used; // Whether the mapping is in use, or on the freelist. + int buf; // The buffer the mapping is using. + int off; // Offset of the mapped region. + int size; // Size of the mapped region. + int width, height, stride; // Image properties. + }; + std::vector<Mapping> mappings; + + /* + * A Buffer cycles through the following five states: + * + * 1. Current --> We are currently filling this buffer up with allocations. + * 2. Not current, refs > 0 --> Finished the above, but may still be writing into it and issuing GL commands from it. + * 3. Not current, refs == 0, !ready, !sync --> Finished the above, but GL may be reading from it. We have yet to create its sync object. + * 4. Not current, refs == 0, !ready, sync --> We have now created its sync object, but it has not been signalled yet. + * 5. Not current, refs == 0, ready --> The sync object has been signalled and deleted. + * + * Only one Buffer is Current at any given time, and is marked by the current_buffer variable. + */ + +public: + PersistentPixelStreamer() + { + // Create a single initial buffer and make it the current buffer. + buffers.emplace_back(); + buffers.back().create(); + current_buffer = 0; + } + + Method get_method() const override { return Method::Persistent; } + + Cairo::RefPtr<Cairo::ImageSurface> request(Geom::IntPoint const &dimensions, bool nogl) override + { + // Calculate image properties required by cairo. + int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, dimensions.x()); + int size = stride * dimensions.y(); + int sizeup = Util::roundup(size, 64); + assert(sizeup < bufsize); + + // Attempt to advance buffers in states 3 or 4 towards 5, if allowed. + if (!nogl) { + for (int i = 0; i < buffers.size(); i++) { + if (i != current_buffer && buffers[i].refs == 0 && !buffers[i].ready) { + buffers[i].advance(); + } + } + } + // Continue using the current buffer if possible. + if (buffers[current_buffer].off + sizeup <= bufsize) { + goto chosen_buffer; + } + // Otherwise, the current buffer has filled up. After this point, the current buffer will change. + // Therefore, handle the state change of the current buffer out of the Current state. Usually that + // means doing nothing because the transition to state 2 is automatic. But if refs == 0 already, + // then we need to transition into state 3 by setting ready = false. If we're allowed to use GL, + // then we can additionally transition into state 4 by creating the sync object. + if (buffers[current_buffer].refs == 0) { + buffers[current_buffer].ready = false; + buffers[current_buffer].sync = nogl ? nullptr : glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + } + // Attempt to re-use a old buffer that has reached state 5. + for (int i = 0; i < buffers.size(); i++) { + if (i != current_buffer && buffers[i].refs == 0 && buffers[i].ready) { + // Found an unused buffer. Re-use it. (Move to state 1.) + buffers[i].off = 0; + current_buffer = i; + goto chosen_buffer; + } + } + // Otherwise, there are no available buffers. Create and use a new one. That requires GL, so fail if not allowed. + if (nogl) { + return {}; + } + buffers.emplace_back(); + buffers.back().create(); + current_buffer = buffers.size() - 1; + chosen_buffer: + // Finished changing the current buffer. + auto &b = buffers[current_buffer]; + + // Choose/create the mapping to use. + auto choose_mapping = [&, this] { + for (int i = 0; i < mappings.size(); i++) { + if (!mappings[i].used) { + // Found unused mapping. + return i; + } + } + // No free mapping; create one. + mappings.emplace_back(); + return (int)mappings.size() - 1; + }; + + auto mapping = choose_mapping(); + auto &m = mappings[mapping]; + + // Set up the mapping bookkeeping. + m = {true, current_buffer, b.off, size, dimensions.x(), dimensions.y(), stride}; + b.off += sizeup; + b.refs++; + + // Create the image surface. + auto surface = Cairo::ImageSurface::create(b.data + m.off, Cairo::FORMAT_ARGB32, dimensions.x(), dimensions.y(), stride); + + // Attach the mapping handle as user data. + cairo_surface_set_user_data(surface->cobj(), &key, (void*)(uintptr_t)mapping, nullptr); + + return surface; + } + + void finish(Cairo::RefPtr<Cairo::ImageSurface> surface, bool junk) override + { + // Extract the mapping handle from the surface's user data. + auto mapping = (int)(uintptr_t)cairo_surface_get_user_data(surface->cobj(), &key); + + // Flush all changes from the image surface to the buffer, and delete it. + surface.clear(); + + auto &m = mappings[mapping]; + auto &b = buffers[m.buf]; + + // Flush the mapped subregion. + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, b.pbo); + glFlushMappedBufferRange(GL_PIXEL_UNPACK_BUFFER, m.off, m.size); + + // Tear down the mapping bookkeeping. (if this causes transition 2 --> 3, it is handled below.) + m.used = false; + b.refs--; + + // Upload to the texture from the mapped subregion. + if (!junk) { + glPixelStorei(GL_UNPACK_ROW_LENGTH, m.stride / 4); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m.width, m.height, GL_BGRA, GL_UNSIGNED_BYTE, (void*)(uintptr_t)m.off); + } + + // If the buffer is due for recycling, issue a sync command so that we can recycle it when it's ready. (Handle transition 2 --> 4.) + if (m.buf != current_buffer && b.refs == 0) { + b.ready = false; + b.sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + } + + // Check other buffers to see if they're ready for recycling. (Advance from 3/4 towards 5.) + for (int i = 0; i < buffers.size(); i++) { + if (i != current_buffer && i != m.buf && buffers[i].refs == 0 && !buffers[i].ready) { + buffers[i].advance(); + } + } + } + + ~PersistentPixelStreamer() override + { + // Delete any sync objects. (For buffers in state 4.) + for (int i = 0; i < buffers.size(); i++) { + if (i != current_buffer && buffers[i].refs == 0 && !buffers[i].ready && buffers[i].sync) { + glDeleteSync(buffers[i].sync); + } + } + + // Wait for GL to finish reading out of all the buffers. + glFinish(); + + // Deallocate the buffers on the GL side. + for (auto &b : buffers) { + b.destroy(); + } + } +}; + +class AsynchronousPixelStreamer : public PixelStreamer +{ + static int constexpr minbufsize = 0x4000; // 16 KiB + static int constexpr expire_timeout = 10000; + + static int constexpr size_to_bucket(int size) { return Util::floorlog2((size - 1) / minbufsize) + 1; } + static int constexpr bucket_maxsize(int b) { return minbufsize * (1 << b); } + + struct Buffer + { + GLuint pbo; + unsigned char *data; + + void create(int size) + { + glGenBuffers(1, &pbo); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo); + glBufferData(GL_PIXEL_UNPACK_BUFFER, size, nullptr, GL_STREAM_DRAW); + data = (unsigned char*)glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, size, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT); + } + + void destroy() + { + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo); + glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); + glDeleteBuffers(1, &pbo); + } + }; + + struct Bucket + { + std::vector<Buffer> spares; + int used = 0; + int high_use_count = 0; + }; + std::vector<Bucket> buckets; + + struct Mapping + { + bool used; + Buffer buf; + int bucket; + int width, height, stride; + }; + std::vector<Mapping> mappings; + + int expire_timer = 0; + +public: + Method get_method() const override { return Method::Asynchronous; } + + Cairo::RefPtr<Cairo::ImageSurface> request(Geom::IntPoint const &dimensions, bool nogl) override + { + // Calculate image properties required by cairo. + int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, dimensions.x()); + int size = stride * dimensions.y(); + + // Find the bucket that size falls into. + int bucket = size_to_bucket(size); + if (bucket >= buckets.size()) { + buckets.resize(bucket + 1); + } + auto &b = buckets[bucket]; + + // Find/create a buffer of the appropriate size. + Buffer buf; + if (!b.spares.empty()) { + // If the bucket has any spare mapped buffers, then use one of them. + buf = std::move(b.spares.back()); + b.spares.pop_back(); + } else if (!nogl) { + // Otherwise, we have to use OpenGL to create and map a new buffer. + buf.create(bucket_maxsize(bucket)); + } else { + // If we're not allowed to issue GL commands, then that is a failure. + return {}; + } + + // Record the new use count of the bucket. + b.used++; + if (b.used > b.high_use_count) { + // If the use count has gone above the high-water mark, record it and reset the timer for when to clean up excess spares. + b.high_use_count = b.used; + expire_timer = 0; + } + + auto choose_mapping = [&, this] { + for (int i = 0; i < mappings.size(); i++) { + if (!mappings[i].used) { + return i; + } + } + mappings.emplace_back(); + return (int)mappings.size() - 1; + }; + + auto mapping = choose_mapping(); + auto &m = mappings[mapping]; + + m.used = true; + m.buf = std::move(buf); + m.bucket = bucket; + m.width = dimensions.x(); + m.height = dimensions.y(); + m.stride = stride; + + auto surface = Cairo::ImageSurface::create(m.buf.data, Cairo::FORMAT_ARGB32, m.width, m.height, m.stride); + cairo_surface_set_user_data(surface->cobj(), &key, (void*)(uintptr_t)mapping, nullptr); + return surface; + } + + void finish(Cairo::RefPtr<Cairo::ImageSurface> surface, bool junk) override + { + auto mapping = (int)(uintptr_t)cairo_surface_get_user_data(surface->cobj(), &key); + surface.clear(); + + auto &m = mappings[mapping]; + auto &b = buckets[m.bucket]; + + // Unmap the buffer. + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, m.buf.pbo); + glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); + + // Upload the buffer to the texture. + if (!junk) { + glPixelStorei(GL_UNPACK_ROW_LENGTH, m.stride / 4); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m.width, m.height, GL_BGRA, GL_UNSIGNED_BYTE, nullptr); + } + + // Mark the mapping slot as unused. + m.used = false; + + // Orphan and re-map the buffer. + auto size = bucket_maxsize(m.bucket); + glBufferData(GL_PIXEL_UNPACK_BUFFER, size, nullptr, GL_STREAM_DRAW); + m.buf.data = (unsigned char*)glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, size, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT); + + // Put the buffer back in its corresponding bucket's pile of spares. + b.spares.emplace_back(std::move(m.buf)); + b.used--; + + // If the expiration timeout has been reached, get rid of excess spares from all buckets, and reset the high use counts. + expire_timer++; + if (expire_timer >= expire_timeout) { + expire_timer = 0; + + for (auto &b : buckets) { + int max_spares = b.high_use_count - b.used; + assert(max_spares >= 0); + if (b.spares.size() > max_spares) { + for (int i = max_spares; i < b.spares.size(); i++) { + b.spares[i].destroy(); + } + b.spares.resize(max_spares); + } + b.high_use_count = b.used; + } + } + } + + ~AsynchronousPixelStreamer() override + { + // Unmap and delete all spare buffers. (They are not being used.) + for (auto &b : buckets) { + for (auto &buf : b.spares) { + buf.destroy(); + } + } + } +}; + +class SynchronousPixelStreamer : public PixelStreamer +{ + struct Mapping + { + bool used; + std::vector<unsigned char> data; + int size, width, height, stride; + }; + std::vector<Mapping> mappings; + +public: + Method get_method() const override { return Method::Synchronous; } + + Cairo::RefPtr<Cairo::ImageSurface> request(Geom::IntPoint const &dimensions, bool) override + { + auto choose_mapping = [&, this] { + for (int i = 0; i < mappings.size(); i++) { + if (!mappings[i].used) { + return i; + } + } + mappings.emplace_back(); + return (int)mappings.size() - 1; + }; + + auto mapping = choose_mapping(); + auto &m = mappings[mapping]; + + m.used = true; + m.width = dimensions.x(); + m.height = dimensions.y(); + m.stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, m.width); + m.size = m.stride * m.height; + m.data.resize(m.size); + + auto surface = Cairo::ImageSurface::create(&m.data[0], Cairo::FORMAT_ARGB32, m.width, m.height, m.stride); + cairo_surface_set_user_data(surface->cobj(), &key, (void*)(uintptr_t)mapping, nullptr); + return surface; + } + + void finish(Cairo::RefPtr<Cairo::ImageSurface> surface, bool junk) override + { + auto mapping = (int)(uintptr_t)cairo_surface_get_user_data(surface->cobj(), &key); + surface.clear(); + + auto &m = mappings[mapping]; + + if (!junk) { + glPixelStorei(GL_UNPACK_ROW_LENGTH, m.stride / 4); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m.width, m.height, GL_BGRA, GL_UNSIGNED_BYTE, &m.data[0]); + } + + m.used = false; + m.data.clear(); + } +}; + +} // namespace + +std::unique_ptr<PixelStreamer> PixelStreamer::create_supported(Method method) +{ + int ver = epoxy_gl_version(); + + if (method <= Method::Asynchronous) { + if (ver >= 30 || epoxy_has_gl_extension("GL_ARB_map_buffer_range")) { + if (method <= Method::Persistent) { + if (ver >= 44 || (epoxy_has_gl_extension("GL_ARB_buffer_storage") && + epoxy_has_gl_extension("GL_ARB_texture_storage") && + epoxy_has_gl_extension("GL_ARB_SYNC"))) + { + return std::make_unique<PersistentPixelStreamer>(); + } else if (method != Method::Auto) { + std::cerr << "Persistent PixelStreamer not available" << std::endl; + } + } + return std::make_unique<AsynchronousPixelStreamer>(); + } else if (method != Method::Auto) { + std::cerr << "Asynchronous PixelStreamer not available" << std::endl; + } + } + return std::make_unique<SynchronousPixelStreamer>(); +} + +} // namespace Widget +} // namespace UI +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : |