Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
pixelstreamer.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
2#include <cassert>
3#include <cmath>
4#include <vector>
5#include <epoxy/gl.h>
6#include "pixelstreamer.h"
7#include "helper/mathfns.h"
8
9namespace Inkscape {
10namespace UI {
11namespace Widget {
12namespace {
13
14cairo_user_data_key_t constexpr key{};
15
16class PersistentPixelStreamer : public PixelStreamer
17{
18 static int constexpr bufsize = 0x1000000; // 16 MiB
19
20 struct Buffer
21 {
22 GLuint pbo; // Pixel buffer object.
23 unsigned char *data; // The pointer to the mapped region.
24 int off; // Offset of the unused region, in bytes. Always a multiple of 64.
25 int refs; // How many mappings are currently using this buffer.
26 GLsync sync; // Sync object for telling us when the GPU has finished reading from this buffer.
27 bool ready; // Whether this buffer is ready for re-use.
28
29 void create()
30 {
31 glGenBuffers(1, &pbo);
32 glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
33 glBufferStorage(GL_PIXEL_UNPACK_BUFFER, bufsize, nullptr, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT);
34 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);
35 off = 0;
36 refs = 0;
37 }
38
39 void destroy()
40 {
41 glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
42 glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
43 glDeleteBuffers(1, &pbo);
44 }
45
46 // Advance a buffer in state 3 or 4 as far as possible towards state 5.
47 void advance()
48 {
49 if (!sync) {
50 sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
51 } else {
52 auto ret = glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, 0);
53 if (ret == GL_CONDITION_SATISFIED || ret == GL_ALREADY_SIGNALED) {
54 glDeleteSync(sync);
55 ready = true;
56 }
57 }
58 }
59 };
60 std::vector<Buffer> buffers;
61
63
64 struct Mapping
65 {
66 bool used; // Whether the mapping is in use, or on the freelist.
67 int buf; // The buffer the mapping is using.
68 int off; // Offset of the mapped region.
69 int size; // Size of the mapped region.
70 int width, height, stride; // Image properties.
71 };
72 std::vector<Mapping> mappings;
73
74 /*
75 * A Buffer cycles through the following five states:
76 *
77 * 1. Current --> We are currently filling this buffer up with allocations.
78 * 2. Not current, refs > 0 --> Finished the above, but may still be writing into it and issuing GL commands from it.
79 * 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.
80 * 4. Not current, refs == 0, !ready, sync --> We have now created its sync object, but it has not been signalled yet.
81 * 5. Not current, refs == 0, ready --> The sync object has been signalled and deleted.
82 *
83 * Only one Buffer is Current at any given time, and is marked by the current_buffer variable.
84 */
85
86public:
87 PersistentPixelStreamer()
88 {
89 // Create a single initial buffer and make it the current buffer.
90 buffers.emplace_back();
91 buffers.back().create();
93 }
94
95 Method get_method() const override { return Method::Persistent; }
96
97 Cairo::RefPtr<Cairo::ImageSurface> request(Geom::IntPoint const &dimensions, bool nogl) override
98 {
99 // Calculate image properties required by cairo.
100 int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, dimensions.x());
101 int size = stride * dimensions.y();
102 int sizeup = Util::round_up(size, 64);
103 assert(sizeup < bufsize);
104
105 // Attempt to advance buffers in states 3 or 4 towards 5, if allowed.
106 if (!nogl) {
107 for (int i = 0; i < buffers.size(); i++) {
108 if (i != current_buffer && buffers[i].refs == 0 && !buffers[i].ready) {
109 buffers[i].advance();
110 }
111 }
112 }
113 // Continue using the current buffer if possible.
114 if (buffers[current_buffer].off + sizeup <= bufsize) {
115 goto chosen_buffer;
116 }
117 // Otherwise, the current buffer has filled up. After this point, the current buffer will change.
118 // Therefore, handle the state change of the current buffer out of the Current state. Usually that
119 // means doing nothing because the transition to state 2 is automatic. But if refs == 0 already,
120 // then we need to transition into state 3 by setting ready = false. If we're allowed to use GL,
121 // then we can additionally transition into state 4 by creating the sync object.
122 if (buffers[current_buffer].refs == 0) {
123 buffers[current_buffer].ready = false;
124 buffers[current_buffer].sync = nogl ? nullptr : glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
125 }
126 // Attempt to re-use a old buffer that has reached state 5.
127 for (int i = 0; i < buffers.size(); i++) {
128 if (i != current_buffer && buffers[i].refs == 0 && buffers[i].ready) {
129 // Found an unused buffer. Re-use it. (Move to state 1.)
130 buffers[i].off = 0;
131 current_buffer = i;
132 goto chosen_buffer;
133 }
134 }
135 // Otherwise, there are no available buffers. Create and use a new one. That requires GL, so fail if not allowed.
136 if (nogl) {
137 return {};
138 }
139 buffers.emplace_back();
140 buffers.back().create();
141 current_buffer = buffers.size() - 1;
142 chosen_buffer:
143 // Finished changing the current buffer.
144 auto &b = buffers[current_buffer];
145
146 // Choose/create the mapping to use.
147 auto choose_mapping = [&, this] {
148 for (int i = 0; i < mappings.size(); i++) {
149 if (!mappings[i].used) {
150 // Found unused mapping.
151 return i;
152 }
153 }
154 // No free mapping; create one.
155 mappings.emplace_back();
156 return (int)mappings.size() - 1;
157 };
158
159 auto mapping = choose_mapping();
160 auto &m = mappings[mapping];
161
162 // Set up the mapping bookkeeping.
163 m = {true, current_buffer, b.off, size, dimensions.x(), dimensions.y(), stride};
164 b.off += sizeup;
165 b.refs++;
166
167 // Create the image surface.
168 auto surface = Cairo::ImageSurface::create(b.data + m.off, Cairo::Surface::Format::ARGB32, dimensions.x(), dimensions.y(), stride);
169
170 // Attach the mapping handle as user data.
171 cairo_surface_set_user_data(surface->cobj(), &key, (void*)(uintptr_t)mapping, nullptr);
172
173 return surface;
174 }
175
176 void finish(Cairo::RefPtr<Cairo::ImageSurface> surface, bool junk) override
177 {
178 // Extract the mapping handle from the surface's user data.
179 auto mapping = (int)(uintptr_t)cairo_surface_get_user_data(surface->cobj(), &key);
180
181 // Flush all changes from the image surface to the buffer, and delete it.
182 surface.reset();
183
184 auto &m = mappings[mapping];
185 auto &b = buffers[m.buf];
186
187 // Flush the mapped subregion.
188 glBindBuffer(GL_PIXEL_UNPACK_BUFFER, b.pbo);
189 glFlushMappedBufferRange(GL_PIXEL_UNPACK_BUFFER, m.off, m.size);
190
191 // Tear down the mapping bookkeeping. (if this causes transition 2 --> 3, it is handled below.)
192 m.used = false;
193 b.refs--;
194
195 // Upload to the texture from the mapped subregion.
196 if (!junk) {
197 glPixelStorei(GL_UNPACK_ROW_LENGTH, m.stride / 4);
198 glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m.width, m.height, GL_BGRA, GL_UNSIGNED_BYTE, (void*)(uintptr_t)m.off);
199 }
200
201 // 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.)
202 if (m.buf != current_buffer && b.refs == 0) {
203 b.ready = false;
204 b.sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
205 }
206
207 // Check other buffers to see if they're ready for recycling. (Advance from 3/4 towards 5.)
208 for (int i = 0; i < buffers.size(); i++) {
209 if (i != current_buffer && i != m.buf && buffers[i].refs == 0 && !buffers[i].ready) {
210 buffers[i].advance();
211 }
212 }
213 }
214
215 ~PersistentPixelStreamer() override
216 {
217 // Delete any sync objects. (For buffers in state 4.)
218 for (int i = 0; i < buffers.size(); i++) {
219 if (i != current_buffer && buffers[i].refs == 0 && !buffers[i].ready && buffers[i].sync) {
220 glDeleteSync(buffers[i].sync);
221 }
222 }
223
224 // Wait for GL to finish reading out of all the buffers.
225 glFinish();
226
227 // Deallocate the buffers on the GL side.
228 for (auto &b : buffers) {
229 b.destroy();
230 }
231 }
232};
233
234class AsynchronousPixelStreamer : public PixelStreamer
235{
236 static int constexpr minbufsize = 0x4000; // 16 KiB
237 static int constexpr expire_timeout = 10000;
238
239 static int constexpr size_to_bucket(int size) { return Util::floorlog2((size - 1) / minbufsize) + 1; }
240 static int constexpr bucket_maxsize(int b) { return minbufsize * (1 << b); }
241
242 struct Buffer
243 {
244 GLuint pbo;
245 unsigned char *data;
246
247 void create(int size)
248 {
249 glGenBuffers(1, &pbo);
250 glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
251 glBufferData(GL_PIXEL_UNPACK_BUFFER, size, nullptr, GL_STREAM_DRAW);
252 data = (unsigned char*)glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, size, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT);
253 }
254
255 void destroy()
256 {
257 glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
258 glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
259 glDeleteBuffers(1, &pbo);
260 }
261 };
262
263 struct Bucket
264 {
265 std::vector<Buffer> spares;
266 int used = 0;
268 };
269 std::vector<Bucket> buckets;
270
271 struct Mapping
272 {
273 bool used;
274 Buffer buf;
276 int width, height, stride;
277 };
278 std::vector<Mapping> mappings;
279
281
282public:
283 Method get_method() const override { return Method::Asynchronous; }
284
285 Cairo::RefPtr<Cairo::ImageSurface> request(Geom::IntPoint const &dimensions, bool nogl) override
286 {
287 // Calculate image properties required by cairo.
288 int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, dimensions.x());
289 int size = stride * dimensions.y();
290
291 // Find the bucket that size falls into.
292 int bucket = size_to_bucket(size);
293 if (bucket >= buckets.size()) {
294 buckets.resize(bucket + 1);
295 }
296 auto &b = buckets[bucket];
297
298 // Find/create a buffer of the appropriate size.
299 Buffer buf;
300 if (!b.spares.empty()) {
301 // If the bucket has any spare mapped buffers, then use one of them.
302 buf = std::move(b.spares.back());
303 b.spares.pop_back();
304 } else if (!nogl) {
305 // Otherwise, we have to use OpenGL to create and map a new buffer.
306 buf.create(bucket_maxsize(bucket));
307 } else {
308 // If we're not allowed to issue GL commands, then that is a failure.
309 return {};
310 }
311
312 // Record the new use count of the bucket.
313 b.used++;
314 if (b.used > b.high_use_count) {
315 // If the use count has gone above the high-water mark, record it and reset the timer for when to clean up excess spares.
316 b.high_use_count = b.used;
317 expire_timer = 0;
318 }
319
320 auto choose_mapping = [&, this] {
321 for (int i = 0; i < mappings.size(); i++) {
322 if (!mappings[i].used) {
323 return i;
324 }
325 }
326 mappings.emplace_back();
327 return (int)mappings.size() - 1;
328 };
329
330 auto mapping = choose_mapping();
331 auto &m = mappings[mapping];
332
333 m.used = true;
334 m.buf = std::move(buf);
335 m.bucket = bucket;
336 m.width = dimensions.x();
337 m.height = dimensions.y();
338 m.stride = stride;
339
340 auto surface = Cairo::ImageSurface::create(m.buf.data, Cairo::Surface::Format::ARGB32, m.width, m.height, m.stride);
341 cairo_surface_set_user_data(surface->cobj(), &key, (void*)(uintptr_t)mapping, nullptr);
342 return surface;
343 }
344
345 void finish(Cairo::RefPtr<Cairo::ImageSurface> surface, bool junk) override
346 {
347 auto mapping = (int)(uintptr_t)cairo_surface_get_user_data(surface->cobj(), &key);
348 surface.reset();
349
350 auto &m = mappings[mapping];
351 auto &b = buckets[m.bucket];
352
353 // Unmap the buffer.
354 glBindBuffer(GL_PIXEL_UNPACK_BUFFER, m.buf.pbo);
355 glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
356
357 // Upload the buffer to the texture.
358 if (!junk) {
359 glPixelStorei(GL_UNPACK_ROW_LENGTH, m.stride / 4);
360 glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m.width, m.height, GL_BGRA, GL_UNSIGNED_BYTE, nullptr);
361 }
362
363 // Mark the mapping slot as unused.
364 m.used = false;
365
366 // Orphan and re-map the buffer.
367 auto size = bucket_maxsize(m.bucket);
368 glBufferData(GL_PIXEL_UNPACK_BUFFER, size, nullptr, GL_STREAM_DRAW);
369 m.buf.data = (unsigned char*)glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, size, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT);
370
371 // Put the buffer back in its corresponding bucket's pile of spares.
372 b.spares.emplace_back(std::move(m.buf));
373 b.used--;
374
375 // If the expiration timeout has been reached, get rid of excess spares from all buckets, and reset the high use counts.
376 expire_timer++;
377 if (expire_timer >= expire_timeout) {
378 expire_timer = 0;
379
380 for (auto &b : buckets) {
381 int max_spares = b.high_use_count - b.used;
382 assert(max_spares >= 0);
383 if (b.spares.size() > max_spares) {
384 for (int i = max_spares; i < b.spares.size(); i++) {
385 b.spares[i].destroy();
386 }
387 b.spares.resize(max_spares);
388 }
389 b.high_use_count = b.used;
390 }
391 }
392 }
393
394 ~AsynchronousPixelStreamer() override
395 {
396 // Unmap and delete all spare buffers. (They are not being used.)
397 for (auto &b : buckets) {
398 for (auto &buf : b.spares) {
399 buf.destroy();
400 }
401 }
402 }
403};
404
405class SynchronousPixelStreamer : public PixelStreamer
406{
407 struct Mapping
408 {
409 bool used;
410 std::vector<unsigned char> data;
411 int size, width, height, stride;
412 };
413 std::vector<Mapping> mappings;
414
415public:
416 Method get_method() const override { return Method::Synchronous; }
417
418 Cairo::RefPtr<Cairo::ImageSurface> request(Geom::IntPoint const &dimensions, bool) override
419 {
420 auto choose_mapping = [&, this] {
421 for (int i = 0; i < mappings.size(); i++) {
422 if (!mappings[i].used) {
423 return i;
424 }
425 }
426 mappings.emplace_back();
427 return (int)mappings.size() - 1;
428 };
429
430 auto mapping = choose_mapping();
431 auto &m = mappings[mapping];
432
433 m.used = true;
434 m.width = dimensions.x();
435 m.height = dimensions.y();
436 m.stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, m.width);
437 m.size = m.stride * m.height;
438 m.data.resize(m.size);
439
440 auto surface = Cairo::ImageSurface::create(&m.data[0], Cairo::Surface::Format::ARGB32, m.width, m.height, m.stride);
441 cairo_surface_set_user_data(surface->cobj(), &key, (void*)(uintptr_t)mapping, nullptr);
442 return surface;
443 }
444
445 void finish(Cairo::RefPtr<Cairo::ImageSurface> surface, bool junk) override
446 {
447 auto mapping = (int)(uintptr_t)cairo_surface_get_user_data(surface->cobj(), &key);
448 surface.reset();
449
450 auto &m = mappings[mapping];
451
452 if (!junk) {
453 glPixelStorei(GL_UNPACK_ROW_LENGTH, m.stride / 4);
454 glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m.width, m.height, GL_BGRA, GL_UNSIGNED_BYTE, &m.data[0]);
455 }
456
457 m.used = false;
458 m.data.clear();
459 }
460};
461
462} // namespace
463
464std::unique_ptr<PixelStreamer> PixelStreamer::create_supported(Method method)
465{
466 int ver = epoxy_gl_version();
467
468 if (method <= Method::Asynchronous) {
469 if (ver >= 30 || epoxy_has_gl_extension("GL_ARB_map_buffer_range")) {
470 if (method <= Method::Persistent) {
471 if (ver >= 44 || (epoxy_has_gl_extension("GL_ARB_buffer_storage") &&
472 epoxy_has_gl_extension("GL_ARB_texture_storage") &&
473 epoxy_has_gl_extension("GL_ARB_SYNC")))
474 {
475 return std::make_unique<PersistentPixelStreamer>();
476 } else if (method != Method::Auto) {
477 std::cerr << "Persistent PixelStreamer not available" << std::endl;
478 }
479 }
480 return std::make_unique<AsynchronousPixelStreamer>();
481 } else if (method != Method::Auto) {
482 std::cerr << "Asynchronous PixelStreamer not available" << std::endl;
483 }
484 }
485 return std::make_unique<SynchronousPixelStreamer>();
486}
487
488} // namespace Widget
489} // namespace UI
490} // namespace Inkscape
491
492/*
493 Local Variables:
494 mode:c++
495 c-file-style:"stroustrup"
496 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
497 indent-tabs-mode:nil
498 fill-column:99
499 End:
500*/
501// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
Cairo::RefPtr< Cairo::ImageSurface > surface
Definition canvas.cpp:137
Two-dimensional point with integer coordinates.
Definition int-point.h:57
constexpr IntCoord x() const noexcept
Definition int-point.h:77
constexpr IntCoord y() const noexcept
Definition int-point.h:79
static std::unique_ptr< PixelStreamer > create_supported(Method method)
int constexpr floorlog2(T x)
Returns floor(log_2(x)), assuming x >= 1.
Definition mathfns.h:62
T constexpr round_up(T a, T b)
Returns a rounded up to the nearest multiple of b, assuming b >= 1.
Definition mathfns.h:89
Helper class to stream background task notifications as a series of messages.
static cairo_user_data_key_t key
int stride
std::vector< Bucket > buckets
int refs
std::vector< Buffer > spares
int current_buffer
int size
int buf
bool ready
static int constexpr minbufsize
std::vector< Mapping > mappings
int off
int expire_timer
int high_use_count
GLuint pbo
unsigned char * data
bool used
std::vector< Buffer > buffers
static int constexpr expire_timeout
static int constexpr bufsize
int bucket
GLsync sync
double height
double width
std::unique_ptr< Toolbar >(* create)()
Definition toolbars.cpp:56
Geom::IntPoint dimensions(const Cairo::RefPtr< Cairo::ImageSurface > &surface)
Definition util.cpp:367