summaryrefslogtreecommitdiffstats
path: root/src/ui/widget/canvas
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/widget/canvas')
-rw-r--r--src/ui/widget/canvas/cairographics.cpp423
-rw-r--r--src/ui/widget/canvas/cairographics.h80
-rw-r--r--src/ui/widget/canvas/fragment.h33
-rw-r--r--src/ui/widget/canvas/framecheck.cpp24
-rw-r--r--src/ui/widget/canvas/framecheck.h58
-rw-r--r--src/ui/widget/canvas/glgraphics.cpp873
-rw-r--r--src/ui/widget/canvas/glgraphics.h144
-rw-r--r--src/ui/widget/canvas/graphics.cpp166
-rw-r--r--src/ui/widget/canvas/graphics.h91
-rw-r--r--src/ui/widget/canvas/pixelstreamer.cpp501
-rw-r--r--src/ui/widget/canvas/pixelstreamer.h75
-rw-r--r--src/ui/widget/canvas/prefs.h102
-rw-r--r--src/ui/widget/canvas/stores.cpp371
-rw-r--r--src/ui/widget/canvas/stores.h106
-rw-r--r--src/ui/widget/canvas/synchronizer.cpp103
-rw-r--r--src/ui/widget/canvas/synchronizer.h72
-rw-r--r--src/ui/widget/canvas/texture.cpp66
-rw-r--r--src/ui/widget/canvas/texture.h62
-rw-r--r--src/ui/widget/canvas/texturecache.cpp115
-rw-r--r--src/ui/widget/canvas/texturecache.h52
-rw-r--r--src/ui/widget/canvas/updaters.cpp235
-rw-r--r--src/ui/widget/canvas/updaters.h78
-rw-r--r--src/ui/widget/canvas/util.cpp70
-rw-r--r--src/ui/widget/canvas/util.h75
24 files changed, 3975 insertions, 0 deletions
diff --git a/src/ui/widget/canvas/cairographics.cpp b/src/ui/widget/canvas/cairographics.cpp
new file mode 100644
index 0000000..42b3353
--- /dev/null
+++ b/src/ui/widget/canvas/cairographics.cpp
@@ -0,0 +1,423 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <2geom/parallelogram.h>
+#include "ui/util.h"
+#include "helper/geom.h"
+#include "cairographics.h"
+#include "stores.h"
+#include "prefs.h"
+#include "util.h"
+#include "framecheck.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+CairoGraphics::CairoGraphics(Prefs const &prefs, Stores const &stores, PageInfo const &pi)
+ : prefs(prefs)
+ , stores(stores)
+ , pi(pi) {}
+
+std::unique_ptr<Graphics> Graphics::create_cairo(Prefs const &prefs, Stores const &stores, PageInfo const &pi)
+{
+ return std::make_unique<CairoGraphics>(prefs, stores, pi);
+}
+
+void CairoGraphics::set_outlines_enabled(bool enabled)
+{
+ outlines_enabled = enabled;
+ if (!enabled) {
+ store.outline_surface.clear();
+ snapshot.outline_surface.clear();
+ }
+}
+
+void CairoGraphics::recreate_store(Geom::IntPoint const &dims)
+{
+ auto surface_size = dims * scale_factor;
+
+ auto make_surface = [&, this] {
+ auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, surface_size.x(), surface_size.y());
+ cairo_surface_set_device_scale(surface->cobj(), scale_factor, scale_factor); // No C++ API!
+ return surface;
+ };
+
+ // Recreate the store surface.
+ bool reuse_surface = store.surface && dimensions(store.surface) == surface_size;
+ if (!reuse_surface) {
+ store.surface = make_surface();
+ }
+
+ // Ensure the store surface is filled with the correct default background.
+ if (background_in_stores) {
+ auto cr = Cairo::Context::create(store.surface);
+ paint_background(stores.store(), pi, page, desk, cr);
+ } else if (reuse_surface) {
+ auto cr = Cairo::Context::create(store.surface);
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ cr->paint();
+ }
+
+ // Do the same for the outline surface (except always clearing it to transparent).
+ if (outlines_enabled) {
+ bool reuse_outline_surface = store.outline_surface && dimensions(store.outline_surface) == surface_size;
+ if (!reuse_outline_surface) {
+ store.outline_surface = make_surface();
+ } else {
+ auto cr = Cairo::Context::create(store.outline_surface);
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ cr->paint();
+ }
+ }
+}
+
+void CairoGraphics::shift_store(Fragment const &dest)
+{
+ auto surface_size = dest.rect.dimensions() * scale_factor;
+
+ // Determine the geometry of the shift.
+ auto shift = dest.rect.min() - stores.store().rect.min();
+ auto reuse_rect = (dest.rect & cairo_to_geom(stores.store().drawn->get_extents())).regularized();
+ assert(reuse_rect); // Should not be called if there is no overlap.
+
+ auto make_surface = [&, this] {
+ auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, surface_size.x(), surface_size.y());
+ cairo_surface_set_device_scale(surface->cobj(), scale_factor, scale_factor); // No C++ API!
+ return surface;
+ };
+
+ // Create the new store surface.
+ bool reuse_surface = snapshot.surface && dimensions(snapshot.surface) == surface_size;
+ auto new_surface = reuse_surface ? std::move(snapshot.surface) : make_surface();
+
+ // Paint background into region of store not covered by next operation.
+ auto cr = Cairo::Context::create(new_surface);
+ if (background_in_stores || reuse_surface) {
+ auto reg = Cairo::Region::create(geom_to_cairo(dest.rect));
+ reg->subtract(geom_to_cairo(*reuse_rect));
+ reg->translate(-dest.rect.left(), -dest.rect.top());
+ cr->save();
+ region_to_path(cr, reg);
+ cr->clip();
+ if (background_in_stores) {
+ paint_background(dest, pi, page, desk, cr);
+ } else { // otherwise, reuse_surface is true
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ cr->paint();
+ }
+ cr->restore();
+ }
+
+ // Copy re-usuable contents of old store into new store, shifted.
+ cr->rectangle(reuse_rect->left() - dest.rect.left(), reuse_rect->top() - dest.rect.top(), reuse_rect->width(), reuse_rect->height());
+ cr->clip();
+ cr->set_source(store.surface, -shift.x(), -shift.y());
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->paint();
+
+ // Set the result as the new store surface.
+ snapshot.surface = std::move(store.surface);
+ store.surface = std::move(new_surface);
+
+ // Do the same for the outline store
+ if (outlines_enabled) {
+ // Create.
+ bool reuse_outline_surface = snapshot.outline_surface && dimensions(snapshot.outline_surface) == surface_size;
+ auto new_outline_surface = reuse_outline_surface ? std::move(snapshot.outline_surface) : make_surface();
+ // Background.
+ auto cr = Cairo::Context::create(new_outline_surface);
+ if (reuse_outline_surface) {
+ cr->set_operator(Cairo::OPERATOR_CLEAR);
+ cr->paint();
+ }
+ // Copy.
+ cr->rectangle(reuse_rect->left() - dest.rect.left(), reuse_rect->top() - dest.rect.top(), reuse_rect->width(), reuse_rect->height());
+ cr->clip();
+ cr->set_source(store.outline_surface, -shift.x(), -shift.y());
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->paint();
+ // Set.
+ snapshot.outline_surface = std::move(store.outline_surface);
+ store.outline_surface = std::move(new_outline_surface);
+ }
+}
+
+void CairoGraphics::swap_stores()
+{
+ std::swap(store, snapshot);
+}
+
+void CairoGraphics::fast_snapshot_combine()
+{
+ auto copy = [&, this] (Cairo::RefPtr<Cairo::ImageSurface> const &from,
+ Cairo::RefPtr<Cairo::ImageSurface> const &to) {
+ auto cr = Cairo::Context::create(to);
+ cr->set_antialias(Cairo::ANTIALIAS_NONE);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->translate(-stores.snapshot().rect.left(), -stores.snapshot().rect.top());
+ cr->transform(geom_to_cairo(stores.store().affine.inverse() * stores.snapshot().affine));
+ cr->translate(-1.0, -1.0);
+ region_to_path(cr, shrink_region(stores.store().drawn, 2));
+ cr->translate(1.0, 1.0);
+ cr->clip();
+ cr->set_source(from, stores.store().rect.left(), stores.store().rect.top());
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ cr->paint();
+ };
+
+ copy(store.surface, snapshot.surface);
+ if (outlines_enabled) copy(store.outline_surface, snapshot.outline_surface);
+}
+
+void CairoGraphics::snapshot_combine(Fragment const &dest)
+{
+ // Create the new fragment.
+ auto content_size = dest.rect.dimensions() * scale_factor;
+
+ auto make_surface = [&] {
+ auto result = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, content_size.x(), content_size.y());
+ cairo_surface_set_device_scale(result->cobj(), scale_factor, scale_factor); // No C++ API!
+ return result;
+ };
+
+ CairoFragment fragment;
+ fragment.surface = make_surface();
+ if (outlines_enabled) fragment.outline_surface = make_surface();
+
+ auto copy = [&, this] (Cairo::RefPtr<Cairo::ImageSurface> const &store_from,
+ Cairo::RefPtr<Cairo::ImageSurface> const &snapshot_from,
+ Cairo::RefPtr<Cairo::ImageSurface> const &to, bool background) {
+ auto cr = Cairo::Context::create(to);
+ cr->set_antialias(Cairo::ANTIALIAS_NONE);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ if (background) paint_background(dest, pi, page, desk, cr);
+ cr->translate(-dest.rect.left(), -dest.rect.top());
+ cr->transform(geom_to_cairo(stores.snapshot().affine.inverse() * dest.affine));
+ cr->rectangle(stores.snapshot().rect.left(), stores.snapshot().rect.top(), stores.snapshot().rect.width(), stores.snapshot().rect.height());
+ cr->set_source(snapshot_from, stores.snapshot().rect.left(), stores.snapshot().rect.top());
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ cr->fill();
+ cr->transform(geom_to_cairo(stores.store().affine.inverse() * stores.snapshot().affine));
+ cr->translate(-1.0, -1.0);
+ region_to_path(cr, shrink_region(stores.store().drawn, 2));
+ cr->translate(1.0, 1.0);
+ cr->clip();
+ cr->set_source(store_from, stores.store().rect.left(), stores.store().rect.top());
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ cr->paint();
+ };
+
+ copy(store.surface, snapshot.surface, fragment.surface, background_in_stores);
+ if (outlines_enabled) copy(store.outline_surface, snapshot.outline_surface, fragment.outline_surface, false);
+
+ snapshot = std::move(fragment);
+}
+
+Cairo::RefPtr<Cairo::ImageSurface> CairoGraphics::request_tile_surface(Geom::IntRect const &rect, bool /*nogl*/)
+{
+ // Create temporary surface, isolated from store.
+ auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, rect.width() * scale_factor, rect.height() * scale_factor);
+ cairo_surface_set_device_scale(surface->cobj(), scale_factor, scale_factor);
+ return surface;
+}
+
+void CairoGraphics::draw_tile(Fragment const &fragment, Cairo::RefPtr<Cairo::ImageSurface> surface, Cairo::RefPtr<Cairo::ImageSurface> outline_surface)
+{
+ // Blit from the temporary surface to the store.
+ auto diff = fragment.rect.min() - stores.store().rect.min();
+
+ auto cr = Cairo::Context::create(store.surface);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source(surface, diff.x(), diff.y());
+ cr->rectangle(diff.x(), diff.y(), fragment.rect.width(), fragment.rect.height());
+ cr->fill();
+
+ if (outlines_enabled) {
+ auto cr = Cairo::Context::create(store.outline_surface);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source(outline_surface, diff.x(), diff.y());
+ cr->rectangle(diff.x(), diff.y(), fragment.rect.width(), fragment.rect.height());
+ cr->fill();
+ }
+}
+
+void CairoGraphics::paint_widget(Fragment const &view, PaintArgs const &a, Cairo::RefPtr<Cairo::Context> const &cr)
+{
+ auto f = FrameCheck::Event();
+
+ // Turn off anti-aliasing while compositing the widget for large performance gains. (We can usually
+ // get away with it without any negative visual impact; when we can't, we turn it back on.)
+ cr->set_antialias(Cairo::ANTIALIAS_NONE);
+
+ // Due to a Cairo bug, Cairo sometimes draws outside of its clip region. This results in flickering as Canvas content is drawn
+ // over the bottom scrollbar. This cannot be fixed by setting the correct clip region, as Cairo detects that and turns it into
+ // a no-op. Hence the following workaround, which recreates the clip region from scratch, is required.
+ auto rlist = cairo_copy_clip_rectangle_list(cr->cobj());
+ cr->reset_clip();
+ for (int i = 0; i < rlist->num_rectangles; i++) {
+ cr->rectangle(rlist->rectangles[i].x, rlist->rectangles[i].y, rlist->rectangles[i].width, rlist->rectangles[i].height);
+ }
+ cr->clip();
+ cairo_rectangle_list_destroy(rlist);
+
+ // Draw background if solid colour optimisation is not enabled. (If enabled, it is baked into the stores.)
+ if (!background_in_stores) {
+ if (prefs.debug_framecheck) f = FrameCheck::Event("background");
+ paint_background(view, pi, page, desk, cr);
+ }
+
+ // Even if in solid colour mode, draw the part of background that is not going to be rendered.
+ if (background_in_stores) {
+ auto const &s = stores.mode() == Stores::Mode::Decoupled ? stores.snapshot() : stores.store();
+ if (!(Geom::Parallelogram(s.rect) * s.affine.inverse() * view.affine).contains(view.rect)) {
+ if (prefs.debug_framecheck) f = FrameCheck::Event("background", 2);
+ cr->save();
+ cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD);
+ cr->rectangle(0, 0, view.rect.width(), view.rect.height());
+ cr->translate(-view.rect.left(), -view.rect.top());
+ cr->transform(geom_to_cairo(s.affine.inverse() * view.affine));
+ cr->rectangle(s.rect.left(), s.rect.top(), s.rect.width(), s.rect.height());
+ cr->clip();
+ cr->transform(geom_to_cairo(view.affine.inverse() * s.affine));
+ cr->translate(view.rect.left(), view.rect.top());
+ paint_background(view, pi, page, desk, cr);
+ cr->restore();
+ }
+ }
+
+ auto draw_store = [&, this] (Cairo::RefPtr<Cairo::ImageSurface> const &store, Cairo::RefPtr<Cairo::ImageSurface> const &snapshot_store) {
+ if (stores.mode() == Stores::Mode::Normal) {
+ // Blit store to view.
+ if (prefs.debug_framecheck) f = FrameCheck::Event("draw");
+ cr->save();
+ auto const &r = stores.store().rect;
+ cr->translate(-view.rect.left(), -view.rect.top());
+ cr->transform(geom_to_cairo(stores.store().affine.inverse() * view.affine)); // Almost always the identity.
+ cr->rectangle(r.left(), r.top(), r.width(), r.height());
+ cr->set_source(store, r.left(), r.top());
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ cr->fill();
+ cr->restore();
+ } else {
+ // Draw transformed snapshot, clipped to the complement of the store's clean region.
+ if (prefs.debug_framecheck) f = FrameCheck::Event("composite", 1);
+
+ cr->save();
+ cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD);
+ cr->rectangle(0, 0, view.rect.width(), view.rect.height());
+ cr->translate(-view.rect.left(), -view.rect.top());
+ cr->transform(geom_to_cairo(stores.store().affine.inverse() * view.affine));
+ region_to_path(cr, stores.store().drawn);
+ cr->transform(geom_to_cairo(stores.snapshot().affine.inverse() * stores.store().affine));
+ cr->clip();
+ auto const &r = stores.snapshot().rect;
+ cr->rectangle(r.left(), r.top(), r.width(), r.height());
+ cr->clip();
+ cr->set_source(snapshot_store, r.left(), r.top());
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ cr->paint();
+ if (prefs.debug_show_snapshot) {
+ cr->set_source_rgba(0, 0, 1, 0.2);
+ cr->set_operator(Cairo::OPERATOR_OVER);
+ cr->paint();
+ }
+ cr->restore();
+
+ // Draw transformed store, clipped to drawn region.
+ if (prefs.debug_framecheck) f = FrameCheck::Event("composite", 0);
+ cr->save();
+ cr->translate(-view.rect.left(), -view.rect.top());
+ cr->transform(geom_to_cairo(stores.store().affine.inverse() * view.affine));
+ cr->set_source(store, stores.store().rect.left(), stores.store().rect.top());
+ Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST);
+ region_to_path(cr, stores.store().drawn);
+ cr->fill();
+ cr->restore();
+ }
+ };
+
+ auto draw_overlay = [&, this] {
+ // Get whitewash opacity.
+ double outline_overlay_opacity = prefs.outline_overlay_opacity / 100.0;
+
+ // Partially obscure drawing by painting semi-transparent white, then paint outline content.
+ // Note: Unfortunately this also paints over the background, but this is unavoidable.
+ cr->save();
+ cr->set_operator(Cairo::OPERATOR_OVER);
+ cr->set_source_rgb(1.0, 1.0, 1.0);
+ cr->paint_with_alpha(outline_overlay_opacity);
+ draw_store(store.outline_surface, snapshot.outline_surface);
+ cr->restore();
+ };
+
+ if (a.splitmode == Inkscape::SplitMode::SPLIT) {
+
+ // Calculate the clipping rectangles for split view.
+ auto [store_clip, outline_clip] = calc_splitview_cliprects(view.rect.dimensions(), a.splitfrac, a.splitdir);
+
+ // Draw normal content.
+ cr->save();
+ cr->rectangle(store_clip.left(), store_clip.top(), store_clip.width(), store_clip.height());
+ cr->clip();
+ cr->set_operator(background_in_stores ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER);
+ draw_store(store.surface, snapshot.surface);
+ if (a.render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY) draw_overlay();
+ cr->restore();
+
+ // Draw outline.
+ if (background_in_stores) {
+ cr->save();
+ cr->translate(outline_clip.left(), outline_clip.top());
+ paint_background(Fragment{view.affine, view.rect.min() + outline_clip}, pi, page, desk, cr);
+ cr->restore();
+ }
+ cr->save();
+ cr->rectangle(outline_clip.left(), outline_clip.top(), outline_clip.width(), outline_clip.height());
+ cr->clip();
+ cr->set_operator(Cairo::OPERATOR_OVER);
+ draw_store(store.outline_surface, snapshot.outline_surface);
+ cr->restore();
+
+ } else {
+
+ // Draw the normal content over the whole view.
+ cr->set_operator(background_in_stores ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER);
+ draw_store(store.surface, snapshot.surface);
+ if (a.render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY) draw_overlay();
+
+ // Draw outline if in X-ray mode.
+ if (a.splitmode == Inkscape::SplitMode::XRAY && a.mouse) {
+ // Clip to circle
+ cr->set_antialias(Cairo::ANTIALIAS_DEFAULT);
+ cr->arc(a.mouse->x(), a.mouse->y(), prefs.xray_radius, 0, 2 * M_PI);
+ cr->clip();
+ cr->set_antialias(Cairo::ANTIALIAS_NONE);
+ // Draw background.
+ paint_background(view, pi, page, desk, cr);
+ // Draw outline.
+ cr->set_operator(Cairo::OPERATOR_OVER);
+ draw_store(store.outline_surface, snapshot.outline_surface);
+ }
+ }
+
+ // The rest can be done with antialiasing.
+ cr->set_antialias(Cairo::ANTIALIAS_DEFAULT);
+
+ if (a.splitmode == Inkscape::SplitMode::SPLIT) {
+ paint_splitview_controller(view.rect.dimensions(), a.splitfrac, a.splitdir, a.hoverdir, cr);
+ }
+}
+
+} // 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 :
diff --git a/src/ui/widget/canvas/cairographics.h b/src/ui/widget/canvas/cairographics.h
new file mode 100644
index 0000000..c29eff1
--- /dev/null
+++ b/src/ui/widget/canvas/cairographics.h
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Cairo display backend.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_CAIROGRAPHICS_H
+#define INKSCAPE_UI_WIDGET_CANVAS_CAIROGRAPHICS_H
+
+#include "graphics.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+struct CairoFragment
+{
+ Cairo::RefPtr<Cairo::ImageSurface> surface;
+ Cairo::RefPtr<Cairo::ImageSurface> outline_surface;
+};
+
+class CairoGraphics : public Graphics
+{
+public:
+ CairoGraphics(Prefs const &prefs, Stores const &stores, PageInfo const &pi);
+
+ void set_scale_factor(int scale) override { scale_factor = scale; }
+ void set_outlines_enabled(bool) override;
+ void set_background_in_stores(bool enabled) override { background_in_stores = enabled; }
+ void set_colours(uint32_t p, uint32_t d, uint32_t b) override { page = p; desk = d; border = b; }
+
+ void recreate_store(Geom::IntPoint const &dimensions) override;
+ void shift_store(Fragment const &dest) override;
+ void swap_stores() override;
+ void fast_snapshot_combine() override;
+ void snapshot_combine(Fragment const &dest) override;
+ void invalidate_snapshot() override {}
+
+ bool is_opengl() const override { return false; }
+ void invalidated_glstate() override {}
+
+ Cairo::RefPtr<Cairo::ImageSurface> request_tile_surface(Geom::IntRect const &rect, bool nogl) override;
+ void draw_tile(Fragment const &fragment, Cairo::RefPtr<Cairo::ImageSurface> surface, Cairo::RefPtr<Cairo::ImageSurface> outline_surface) override;
+ void junk_tile_surface(Cairo::RefPtr<Cairo::ImageSurface> surface) override {}
+
+ void paint_widget(Fragment const &view, PaintArgs const &args, Cairo::RefPtr<Cairo::Context> const &cr) override;
+
+private:
+ // Drawn content.
+ CairoFragment store, snapshot;
+
+ // Dependency objects in canvas.
+ Prefs const &prefs;
+ Stores const &stores;
+ PageInfo const &pi;
+
+ // Backend-agnostic state.
+ int scale_factor = 1;
+ bool outlines_enabled = false;
+ bool background_in_stores = false;
+ uint32_t page, desk, border;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_CAIROGRAPHICS_H
+
+/*
+ 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 :
diff --git a/src/ui/widget/canvas/fragment.h b/src/ui/widget/canvas/fragment.h
new file mode 100644
index 0000000..d3edc74
--- /dev/null
+++ b/src/ui/widget/canvas/fragment.h
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_FRAGMENT_H
+#define INKSCAPE_UI_WIDGET_CANVAS_FRAGMENT_H
+
+#include <2geom/int-rect.h>
+#include <2geom/affine.h>
+
+namespace Inkscape::UI::Widget {
+
+/// A "fragment" is a rectangle of drawn content at a specfic place.
+struct Fragment
+{
+ // The matrix the geometry was transformed with when the content was drawn.
+ Geom::Affine affine;
+
+ // The rectangle of world space where the fragment was drawn.
+ Geom::IntRect rect;
+};
+
+} // namespace Inkscape::UI::Widget
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_FRAGMENT_H
+
+/*
+ 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 :
diff --git a/src/ui/widget/canvas/framecheck.cpp b/src/ui/widget/canvas/framecheck.cpp
new file mode 100644
index 0000000..c127c8e
--- /dev/null
+++ b/src/ui/widget/canvas/framecheck.cpp
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <fstream>
+#include <iostream>
+#include <mutex>
+#include <boost/filesystem.hpp> // Using boost::filesystem instead of std::filesystem due to broken C++17 on MacOS.
+#include "framecheck.h"
+namespace fs = boost::filesystem;
+
+namespace Inkscape::FrameCheck {
+
+void Event::write()
+{
+ static std::mutex mutex;
+ static auto logfile = [] {
+ auto path = fs::temp_directory_path() / "framecheck.txt";
+ auto mode = std::ios_base::out | std::ios_base::app | std::ios_base::binary;
+ return std::ofstream(path.string(), mode);
+ }();
+
+ auto lock = std::lock_guard(mutex);
+ logfile << name << ' ' << start << ' ' << g_get_monotonic_time() << ' ' << subtype << std::endl;
+}
+
+} // namespace Inkscape::FrameCheck
diff --git a/src/ui/widget/canvas/framecheck.h b/src/ui/widget/canvas/framecheck.h
new file mode 100644
index 0000000..8964561
--- /dev/null
+++ b/src/ui/widget/canvas/framecheck.h
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_FRAMECHECK_H
+#define INKSCAPE_FRAMECHECK_H
+
+#include <glib.h>
+
+namespace Inkscape::FrameCheck {
+
+/// RAII object that logs a timing event for the duration of its lifetime.
+struct Event
+{
+ gint64 start;
+ char const *name;
+ int subtype;
+
+ Event() : start(-1) {}
+
+ Event(char const *name, int subtype = 0) : start(g_get_monotonic_time()), name(name), subtype(subtype) {}
+
+ Event(Event &&p) { movefrom(p); }
+
+ ~Event() { finish(); }
+
+ Event &operator=(Event &&p)
+ {
+ finish();
+ movefrom(p);
+ return *this;
+ }
+
+private:
+ void movefrom(Event &p)
+ {
+ start = p.start;
+ name = p.name;
+ subtype = p.subtype;
+ p.start = -1;
+ }
+
+ void finish() { if (start != -1) write(); }
+
+ void write();
+};
+
+} // namespace Inkscape::FrameCheck
+
+#endif // INKSCAPE_FRAMECHECK_H
+
+/*
+ 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 :
diff --git a/src/ui/widget/canvas/glgraphics.cpp b/src/ui/widget/canvas/glgraphics.cpp
new file mode 100644
index 0000000..b00503c
--- /dev/null
+++ b/src/ui/widget/canvas/glgraphics.cpp
@@ -0,0 +1,873 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <2geom/transforms.h>
+#include <2geom/rect.h>
+#include "ui/util.h"
+#include "helper/geom.h"
+#include "glgraphics.h"
+#include "stores.h"
+#include "prefs.h"
+#include "pixelstreamer.h"
+#include "util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+namespace {
+
+// 2Geom <-> OpenGL
+
+void geom_to_uniform_mat(Geom::Affine const &affine, GLuint location)
+{
+ glUniformMatrix2fv(location, 1, GL_FALSE, std::begin({(GLfloat)affine[0], (GLfloat)affine[1], (GLfloat)affine[2], (GLfloat)affine[3]}));
+}
+
+void geom_to_uniform_trans(Geom::Affine const &affine, GLuint location)
+{
+ glUniform2fv(location, 1, std::begin({(GLfloat)affine[4], (GLfloat)affine[5]}));
+}
+
+void geom_to_uniform(Geom::Affine const &affine, GLuint mat_location, GLuint trans_location)
+{
+ geom_to_uniform_mat(affine, mat_location);
+ geom_to_uniform_trans(affine, trans_location);
+}
+
+void geom_to_uniform(Geom::Point const &vec, GLuint location)
+{
+ glUniform2fv(location, 1, std::begin({(GLfloat)vec.x(), (GLfloat)vec.y()}));
+}
+
+// Get the affine transformation required to paste fragment A onto fragment B, assuming
+// coordinates such that A is a texture (0 to 1) and B is a framebuffer (-1 to 1).
+static auto calc_paste_transform(Fragment const &a, Fragment const &b)
+{
+ Geom::Affine result = Geom::Scale(a.rect.dimensions());
+
+ if (a.affine == b.affine) {
+ result *= Geom::Translate(a.rect.min() - b.rect.min());
+ } else {
+ result *= Geom::Translate(a.rect.min()) * a.affine.inverse() * b.affine * Geom::Translate(-b.rect.min());
+ }
+
+ return result * Geom::Scale(2.0 / b.rect.dimensions()) * Geom::Translate(-1.0, -1.0);
+}
+
+// Given a region, shrink it by 0.5px, and convert the result to a VAO of triangles.
+static auto region_shrink_vao(Cairo::RefPtr<Cairo::Region> const &reg, Geom::IntRect const &rel)
+{
+ // Shrink the region by 0.5 (translating it by (0.5, 0.5) in the process).
+ auto reg2 = shrink_region(reg, 1);
+
+ // Preallocate the vertex buffer.
+ int nrects = reg2->get_num_rectangles();
+ std::vector<GLfloat> verts;
+ verts.reserve(nrects * 12);
+
+ // Add a vertex to the buffer, transformed to a coordinate system in which the enclosing rectangle 'rel' goes from 0 to 1.
+ // Also shift them up/left by 0.5px; combined with the width/height increase from earlier, this shrinks the region by 0.5px.
+ auto emit_vertex = [&] (Geom::IntPoint const &pt) {
+ verts.emplace_back((pt.x() - 0.5f - rel.left()) / rel.width());
+ verts.emplace_back((pt.y() - 0.5f - rel.top() ) / rel.height());
+ };
+
+ // Todo: Use a better triangulation algorithm here that results in 1) less triangles, and 2) no seaming.
+ for (int i = 0; i < nrects; i++) {
+ auto rect = cairo_to_geom(reg2->get_rectangle(i));
+ for (int j = 0; j < 6; j++) {
+ int constexpr indices[] = {0, 1, 2, 0, 2, 3};
+ emit_vertex(rect.corner(indices[j]));
+ }
+ }
+
+ // Package the data in a VAO.
+ VAO result;
+ glGenBuffers(1, &result.vbuf);
+ glBindBuffer(GL_ARRAY_BUFFER, result.vbuf);
+ glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(GLfloat), verts.data(), GL_STREAM_DRAW);
+ glGenVertexArrays(1, &result.vao);
+ glBindVertexArray(result.vao);
+ glEnableVertexAttribArray(0);
+ glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 2, 0);
+
+ // Return the VAO and the number of rectangles.
+ return std::make_pair(std::move(result), nrects);
+}
+
+auto pref_to_pixelstreamer(int index)
+{
+ auto constexpr arr = std::array{PixelStreamer::Method::Auto,
+ PixelStreamer::Method::Persistent,
+ PixelStreamer::Method::Asynchronous,
+ PixelStreamer::Method::Synchronous};
+ assert(1 <= index && index <= arr.size());
+ return arr[index - 1];
+}
+
+} // namespace
+
+GLGraphics::GLGraphics(Prefs const &prefs, Stores const &stores, PageInfo const &pi)
+ : prefs(prefs)
+ , stores(stores)
+ , pi(pi)
+{
+ // Create rectangle geometry.
+ GLfloat constexpr verts[] = {0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f};
+ glGenBuffers(1, &rect.vbuf);
+ glBindBuffer(GL_ARRAY_BUFFER, rect.vbuf);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
+ glGenVertexArrays(1, &rect.vao);
+ glBindVertexArray(rect.vao);
+ glEnableVertexAttribArray(0);
+ glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 2, 0);
+
+ // Create shader programs.
+ auto vs = VShader(R"(
+ #version 330 core
+
+ uniform mat2 mat;
+ uniform vec2 trans;
+ uniform vec2 subrect;
+ layout(location = 0) in vec2 pos;
+ smooth out vec2 uv;
+
+ void main()
+ {
+ uv = pos * subrect;
+ vec2 pos2 = mat * pos + trans;
+ gl_Position = vec4(pos2.x, pos2.y, 0.0, 1.0);
+ }
+ )");
+
+ auto texcopy_fs = FShader(R"(
+ #version 330 core
+
+ uniform sampler2D tex;
+ smooth in vec2 uv;
+ out vec4 outColour;
+
+ void main()
+ {
+ outColour = texture(tex, uv);
+ }
+ )");
+
+ auto texcopydouble_fs = FShader(R"(
+ #version 330 core
+
+ uniform sampler2D tex;
+ uniform sampler2D tex_outline;
+ smooth in vec2 uv;
+ layout(location = 0) out vec4 outColour;
+ layout(location = 1) out vec4 outColour_outline;
+
+ void main()
+ {
+ outColour = texture(tex, uv);
+ outColour_outline = texture(tex_outline, uv);
+ }
+ )");
+
+ auto outlineoverlay_fs = FShader(R"(
+ #version 330 core
+
+ uniform sampler2D tex;
+ uniform sampler2D tex_outline;
+ uniform float opacity;
+ smooth in vec2 uv;
+ out vec4 outColour;
+
+ void main()
+ {
+ vec4 c1 = texture(tex, uv);
+ vec4 c2 = texture(tex_outline, uv);
+ vec4 c1w = vec4(mix(c1.rgb, vec3(1.0, 1.0, 1.0) * c1.a, opacity), c1.a);
+ outColour = c1w * (1.0 - c2.a) + c2;
+ }
+ )");
+
+ auto xray_fs = FShader(R"(
+ #version 330 core
+
+ uniform sampler2D tex;
+ uniform sampler2D tex_outline;
+ uniform vec2 pos;
+ uniform float radius;
+ smooth in vec2 uv;
+ out vec4 outColour;
+
+ void main()
+ {
+ vec4 c1 = texture(tex, uv);
+ vec4 c2 = texture(tex_outline, uv);
+
+ float r = length(gl_FragCoord.xy - pos);
+ r = clamp((radius - r) / 2.0, 0.0, 1.0);
+
+ outColour = mix(c1, c2, r);
+ }
+ )");
+
+ auto outlineoverlayxray_fs = FShader(R"(
+ #version 330 core
+
+ uniform sampler2D tex;
+ uniform sampler2D tex_outline;
+ uniform float opacity;
+ uniform vec2 pos;
+ uniform float radius;
+ smooth in vec2 uv;
+ out vec4 outColour;
+
+ void main()
+ {
+ vec4 c1 = texture(tex, uv);
+ vec4 c2 = texture(tex_outline, uv);
+ vec4 c1w = vec4(mix(c1.rgb, vec3(1.0, 1.0, 1.0) * c1.a, opacity), c1.a);
+ outColour = c1w * (1.0 - c2.a) + c2;
+
+ float r = length(gl_FragCoord.xy - pos);
+ r = clamp((radius - r) / 2.0, 0.0, 1.0);
+
+ outColour = mix(outColour, c2, r);
+ }
+ )");
+
+ auto checker_fs = FShader(R"(
+ #version 330 core
+
+ uniform float size;
+ uniform vec3 col1, col2;
+ out vec4 outColour;
+
+ void main()
+ {
+ vec2 a = floor(fract(gl_FragCoord.xy / size) * 2.0);
+ float b = abs(a.x - a.y);
+ outColour = vec4((1.0 - b) * col1 + b * col2, 1.0);
+ }
+ )");
+
+ auto shadow_gs = GShader(R"(
+ #version 330 core
+
+ layout(triangles) in;
+ layout(triangle_strip, max_vertices = 10) out;
+
+ uniform vec2 wh;
+ uniform float size;
+ uniform vec2 dir;
+
+ smooth out vec2 uv;
+ flat out vec2 maxuv;
+
+ void f(vec4 p, vec4 v0, mat2 m)
+ {
+ gl_Position = p;
+ uv = m * (p.xy - v0.xy);
+ EmitVertex();
+ }
+
+ float push(float x)
+ {
+ return 0.15 * (1.0 + clamp(x / 0.707, -1.0, 1.0));
+ }
+
+ void main()
+ {
+ vec4 v0 = gl_in[0].gl_Position;
+ vec4 v1 = gl_in[1].gl_Position;
+ vec4 v2 = gl_in[2].gl_Position;
+ vec4 v3 = gl_in[2].gl_Position - gl_in[1].gl_Position + gl_in[0].gl_Position;
+
+ vec2 a = normalize((v1 - v0).xy * wh);
+ vec2 b = normalize((v3 - v0).xy * wh);
+ float det = a.x * b.y - a.y * b.x;
+ float s = -sign(det);
+ vec2 c = size / abs(det) / wh;
+ vec4 d = vec4(a * c, 0.0, 0.0);
+ vec4 e = vec4(b * c, 0.0, 0.0);
+ mat2 m = s * mat2(a.y, -b.y, -a.x, b.x) * mat2(wh.x, 0.0, 0.0, wh.y) / size;
+
+ float ap = s * dot(vec2(a.y, -a.x), dir);
+ float bp = s * dot(vec2(-b.y, b.x), dir);
+ v0.xy += (b * push( ap) + a * push( bp)) * size / wh;
+ v1.xy += (b * push( ap) + a * -push(-bp)) * size / wh;
+ v2.xy += (b * -push(-ap) + a * -push(-bp)) * size / wh;
+ v3.xy += (b * -push(-ap) + a * push( bp)) * size / wh;
+
+ maxuv = m * (v2.xy - v0.xy);
+ f(v0, v0, m);
+ f(v0 - d - e, v0, m);
+ f(v1, v0, m);
+ f(v1 + d - e, v0, m);
+ f(v2, v0, m);
+ f(v2 + d + e, v0, m);
+ f(v3, v0, m);
+ f(v3 - d + e, v0, m);
+ f(v0, v0, m);
+ f(v0 - d - e, v0, m);
+ EndPrimitive();
+ }
+ )");
+
+ auto shadow_fs = FShader(R"(
+ #version 330 core
+
+ uniform vec4 shadow_col;
+
+ smooth in vec2 uv;
+ flat in vec2 maxuv;
+
+ out vec4 outColour;
+
+ void main()
+ {
+ float x = max(uv.x - maxuv.x, 0.0) - max(-uv.x, 0.0);
+ float y = max(uv.y - maxuv.y, 0.0) - max(-uv.y, 0.0);
+ float s = min(length(vec2(x, y)), 1.0);
+
+ float A = 4.0; // This coefficient changes how steep the curve is and controls shadow drop-off.
+ s = (exp(A * (1.0 - s)) - 1.0) / (exp(A) - 1.0); // Exponential decay for drop shadow - long tail.
+
+ outColour = shadow_col * s;
+ }
+ )");
+
+ texcopy.create(vs, texcopy_fs);
+ texcopydouble.create(vs, texcopydouble_fs);
+ outlineoverlay.create(vs, outlineoverlay_fs);
+ xray.create(vs, xray_fs);
+ outlineoverlayxray.create(vs, outlineoverlayxray_fs);
+ checker.create(vs, checker_fs);
+ shadow.create(vs, shadow_gs, shadow_fs);
+
+ // Create the framebuffer object for rendering to off-view fragments.
+ glGenFramebuffers(1, &fbo);
+
+ // Create the texture cache.
+ texturecache = TextureCache::create();
+
+ // Create the PixelStreamer.
+ pixelstreamer = PixelStreamer::create_supported(pref_to_pixelstreamer(prefs.pixelstreamer_method));
+
+ // Set the last known state as unspecified, forcing a pipeline recreation whatever the next operation is.
+ state = State::None;
+}
+
+GLGraphics::~GLGraphics()
+{
+ glDeleteFramebuffers(1, &fbo);
+}
+
+std::unique_ptr<Graphics> Graphics::create_gl(Prefs const &prefs, Stores const &stores, PageInfo const &pi)
+{
+ return std::make_unique<GLGraphics>(prefs, stores, pi);
+}
+
+void GLGraphics::set_outlines_enabled(bool enabled)
+{
+ outlines_enabled = enabled;
+ if (!enabled) {
+ store.outline_texture.clear();
+ snapshot.outline_texture.clear();
+ }
+}
+
+void GLGraphics::setup_stores_pipeline()
+{
+ if (state == State::Stores) return;
+ state = State::Stores;
+
+ glDisable(GL_BLEND);
+
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
+ GLuint constexpr attachments[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
+ glDrawBuffers(outlines_enabled ? 2 : 1, attachments);
+
+ auto const &shader = outlines_enabled ? texcopydouble : texcopy;
+ glUseProgram(shader.id);
+ mat_loc = shader.loc("mat");
+ trans_loc = shader.loc("trans");
+ geom_to_uniform({1.0, 1.0}, shader.loc("subrect"));
+ tex_loc = shader.loc("tex");
+ if (outlines_enabled) texoutline_loc = shader.loc("tex_outline");
+}
+
+void GLGraphics::recreate_store(Geom::IntPoint const &dims)
+{
+ auto tex_size = dims * scale_factor;
+
+ // Setup the base pipeline.
+ setup_stores_pipeline();
+
+ // Recreate the store textures.
+ auto recreate = [&] (Texture &tex) {
+ if (tex && tex.size() == tex_size) {
+ tex.invalidate();
+ } else {
+ tex = Texture(tex_size);
+ }
+ };
+
+ recreate(store.texture);
+ if (outlines_enabled) {
+ recreate(store.outline_texture);
+ }
+
+ // Bind the store to the framebuffer for writing to.
+ glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, store.texture.id(), 0);
+ if (outlines_enabled) glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, store.outline_texture.id(), 0);
+ glViewport(0, 0, store.texture.size().x(), store.texture.size().y());
+
+ // Clear the store to transparent.
+ glClearColor(0.0, 0.0, 0.0, 0.0);
+ glClear(GL_COLOR_BUFFER_BIT);
+}
+
+void GLGraphics::shift_store(Fragment const &dest)
+{
+ auto tex_size = dest.rect.dimensions() * scale_factor;
+
+ // Setup the base pipeline.
+ setup_stores_pipeline();
+
+ // Create the new fragment.
+ auto create_or_reuse = [&] (Texture &tex, Texture &from) {
+ if (from && from.size() == tex_size) {
+ from.invalidate();
+ tex = std::move(from);
+ } else {
+ tex = Texture(tex_size);
+ }
+ };
+
+ GLFragment fragment;
+ create_or_reuse(fragment.texture, snapshot.texture);
+ if (outlines_enabled) {
+ create_or_reuse(fragment.outline_texture, snapshot.outline_texture);
+ }
+
+ // Bind new store to the framebuffer to writing to.
+ glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fragment.texture .id(), 0);
+ if (outlines_enabled) glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, fragment.outline_texture.id(), 0);
+ glViewport(0, 0, fragment.texture.size().x(), fragment.texture.size().y());
+
+ // Clear new store to transparent.
+ glClearColor(0.0, 0.0, 0.0, 0.0);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ // Bind the old store to texture units 0 and 1 for reading from.
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, store.texture.id());
+ glUniform1i(tex_loc, 0);
+ if (outlines_enabled) {
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_2D, store.outline_texture.id());
+ glUniform1i(texoutline_loc, 1);
+ }
+ glBindVertexArray(rect.vao);
+
+ // Copy re-usuable contents of the old store into the new store.
+ geom_to_uniform(calc_paste_transform(stores.store(), dest), mat_loc, trans_loc);
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+
+ // Set the result as the new store.
+ snapshot = std::move(store);
+ store = std::move(fragment);
+}
+
+void GLGraphics::swap_stores()
+{
+ std::swap(store, snapshot);
+}
+
+void GLGraphics::fast_snapshot_combine()
+{
+ // Ensure the base pipeline is correctly set up.
+ setup_stores_pipeline();
+
+ // Compute the vertex data for the drawn region.
+ auto [clean_vao, clean_numrects] = region_shrink_vao(stores.store().drawn, stores.store().rect);
+
+ // Bind the snapshot to the framebuffer for writing to.
+ glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, snapshot.texture.id(), 0);
+ if (outlines_enabled) glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, snapshot.outline_texture.id(), 0);
+ glViewport(0, 0, snapshot.texture.size().x(), snapshot.texture.size().y());
+
+ // Bind the store to texture unit 0 (and its outline to 1, if necessary).
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, store.texture.id());
+ glUniform1i(tex_loc, 0);
+ if (outlines_enabled) {
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_2D, store.outline_texture.id());
+ glUniform1i(texoutline_loc, 1);
+ }
+
+ // Copy the clean region of the store to the snapshot.
+ geom_to_uniform(calc_paste_transform(stores.store(), stores.snapshot()), mat_loc, trans_loc);
+ glBindVertexArray(clean_vao.vao);
+ glDrawArrays(GL_TRIANGLES, 0, 6 * clean_numrects);
+}
+
+void GLGraphics::snapshot_combine(Fragment const &dest)
+{
+ // Create the new fragment.
+ auto content_size = dest.rect.dimensions() * scale_factor;
+
+ // Ensure the base pipeline is correctly set up.
+ setup_stores_pipeline();
+
+ // Compute the vertex data for the clean region.
+ auto [clean_vao, clean_numrects] = region_shrink_vao(stores.store().drawn, stores.store().rect);
+
+ GLFragment fragment;
+ fragment.texture = Texture(content_size);
+ if (outlines_enabled) fragment.outline_texture = Texture(content_size);
+
+ // Bind the new fragment to the framebuffer for writing to.
+ glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fragment.texture.id(), 0);
+ if (outlines_enabled) glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, fragment.outline_texture.id(), 0);
+
+ // Clear the new fragment to transparent.
+ glViewport(0, 0, fragment.texture.size().x(), fragment.texture.size().y());
+ glClearColor(0.0, 0.0, 0.0, 0.0);
+ glClear(GL_COLOR_BUFFER_BIT);
+
+ // Bind the store and snapshot to texture units 0 and 1 (and their outlines to 2 and 3, if necessary).
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, snapshot.texture.id());
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_2D, store.texture.id());
+ if (outlines_enabled) {
+ glActiveTexture(GL_TEXTURE2);
+ glBindTexture(GL_TEXTURE_2D, snapshot.outline_texture.id());
+ glActiveTexture(GL_TEXTURE3);
+ glBindTexture(GL_TEXTURE_2D, store.outline_texture.id());
+ }
+
+ // Paste the snapshot store onto the new fragment.
+ glUniform1i(tex_loc, 0);
+ if (outlines_enabled) glUniform1i(texoutline_loc, 2);
+ geom_to_uniform(calc_paste_transform(stores.snapshot(), dest), mat_loc, trans_loc);
+ glBindVertexArray(rect.vao);
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+
+ // Paste the backing store onto the new fragment.
+ glUniform1i(tex_loc, 1);
+ if (outlines_enabled) glUniform1i(texoutline_loc, 3);
+ geom_to_uniform(calc_paste_transform(stores.store(), dest), mat_loc, trans_loc);
+ glBindVertexArray(clean_vao.vao);
+ glDrawArrays(GL_TRIANGLES, 0, 6 * clean_numrects);
+
+ // Set the result as the new snapshot.
+ snapshot = std::move(fragment);
+}
+
+void GLGraphics::invalidate_snapshot()
+{
+ if (snapshot.texture) snapshot.texture.invalidate();
+ if (snapshot.outline_texture) snapshot.outline_texture.invalidate();
+}
+
+void GLGraphics::setup_tiles_pipeline()
+{
+ if (state == State::Tiles) return;
+ state = State::Tiles;
+
+ glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
+ GLuint constexpr attachments[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
+ glDrawBuffers(outlines_enabled ? 2 : 1, attachments);
+ glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, store.texture.id(), 0);
+ if (outlines_enabled) glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, store.outline_texture.id(), 0);
+ glViewport(0, 0, store.texture.size().x(), store.texture.size().y());
+
+ auto const &shader = outlines_enabled ? texcopydouble : texcopy;
+ glUseProgram(shader.id);
+ mat_loc = shader.loc("mat");
+ trans_loc = shader.loc("trans");
+ subrect_loc = shader.loc("subrect");
+ glUniform1i(shader.loc("tex"), 0);
+ if (outlines_enabled) glUniform1i(shader.loc("tex_outline"), 1);
+
+ glBindVertexArray(rect.vao);
+ glDisable(GL_BLEND);
+};
+
+Cairo::RefPtr<Cairo::ImageSurface> GLGraphics::request_tile_surface(Geom::IntRect const &rect, bool nogl)
+{
+ Cairo::RefPtr<Cairo::ImageSurface> surface;
+
+ {
+ auto g = std::lock_guard(ps_mutex);
+ surface = pixelstreamer->request(rect.dimensions() * scale_factor, nogl);
+ }
+
+ if (surface) {
+ cairo_surface_set_device_scale(surface->cobj(), scale_factor, scale_factor);
+ }
+
+ return surface;
+}
+
+void GLGraphics::draw_tile(Fragment const &fragment, Cairo::RefPtr<Cairo::ImageSurface> surface, Cairo::RefPtr<Cairo::ImageSurface> outline_surface)
+{
+ auto g = std::lock_guard(ps_mutex);
+ auto surface_size = dimensions(surface);
+
+ Texture texture, outline_texture;
+
+ glActiveTexture(GL_TEXTURE0);
+ texture = texturecache->request(surface_size); // binds
+ pixelstreamer->finish(std::move(surface)); // uploads content
+
+ if (outlines_enabled) {
+ glActiveTexture(GL_TEXTURE1);
+ outline_texture = texturecache->request(surface_size);
+ pixelstreamer->finish(std::move(outline_surface));
+ }
+
+ setup_tiles_pipeline();
+
+ geom_to_uniform(calc_paste_transform(fragment, stores.store()), mat_loc, trans_loc);
+ geom_to_uniform(Geom::Point(surface_size) / texture.size(), subrect_loc);
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+
+ texturecache->finish(std::move(texture));
+ if (outlines_enabled) {
+ texturecache->finish(std::move(outline_texture));
+ }
+}
+
+void GLGraphics::junk_tile_surface(Cairo::RefPtr<Cairo::ImageSurface> surface)
+{
+ auto g = std::lock_guard(ps_mutex);
+ pixelstreamer->finish(std::move(surface), true);
+}
+
+void GLGraphics::setup_widget_pipeline(Fragment const &view)
+{
+ state = State::Widget;
+
+ glDrawBuffer(GL_COLOR_ATTACHMENT0);
+ glViewport(0, 0, view.rect.width() * scale_factor, view.rect.height() * scale_factor);
+ glEnable(GL_STENCIL_TEST);
+ glStencilFunc(GL_NOTEQUAL, 1, 1);
+ glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, store.texture.id());
+ glActiveTexture(GL_TEXTURE1);
+ glBindTexture(GL_TEXTURE_2D, snapshot.texture.id());
+ if (outlines_enabled) {
+ glActiveTexture(GL_TEXTURE2);
+ glBindTexture(GL_TEXTURE_2D, store.outline_texture.id());
+ glActiveTexture(GL_TEXTURE3);
+ glBindTexture(GL_TEXTURE_2D, snapshot.outline_texture.id());
+ }
+ glBindVertexArray(rect.vao);
+};
+
+void GLGraphics::paint_widget(Fragment const &view, PaintArgs const &a, Cairo::RefPtr<Cairo::Context> const&)
+{
+ // If in decoupled mode, create the vertex data describing the drawn region of the store.
+ VAO clean_vao;
+ int clean_numrects;
+ if (stores.mode() == Stores::Mode::Decoupled) {
+ std::tie(clean_vao, clean_numrects) = region_shrink_vao(stores.store().drawn, stores.store().rect);
+ }
+
+ setup_widget_pipeline(view);
+
+ // Clear the buffers. Since we have to pick a clear colour, we choose the page colour, enabling the single-page optimisation later.
+ glClearColor(SP_RGBA32_R_U(page) / 255.0f, SP_RGBA32_G_U(page) / 255.0f, SP_RGBA32_B_U(page) / 255.0f, 1.0);
+ glClear(GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+
+ if (check_single_page(view, pi)) {
+ // A single page occupies the whole view.
+ if (SP_RGBA32_A_U(page) == 255) {
+ // Page is solid - nothing to do, since already cleared to this colour.
+ } else {
+ // Page is checkerboard - fill view with page pattern.
+ glDisable(GL_BLEND);
+ glUseProgram(checker.id);
+ glUniform1f(checker.loc("size"), 12.0 * scale_factor);
+ glUniform3fv(checker.loc("col1"), 1, std::begin(rgb_to_array(page)));
+ glUniform3fv(checker.loc("col2"), 1, std::begin(checkerboard_darken(page)));
+ geom_to_uniform(Geom::Scale(2.0, -2.0) * Geom::Translate(-1.0, 1.0), checker.loc("mat"), checker.loc("trans"));
+ geom_to_uniform({1.0, 1.0}, checker.loc("subrect"));
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+ }
+
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+ } else {
+ glDisable(GL_BLEND);
+
+ auto set_page_transform = [&] (Geom::Rect const &rect, Program const &prog) {
+ geom_to_uniform(Geom::Scale(rect.dimensions()) * Geom::Translate(rect.min()) * calc_paste_transform({{}, Geom::IntRect::from_xywh(0, 0, 1, 1)}, view) * Geom::Scale(1.0, -1.0), prog.loc("mat"), prog.loc("trans"));
+ };
+
+ // Pages
+ glUseProgram(checker.id);
+ glUniform1f(checker.loc("size"), 12.0 * scale_factor);
+ glUniform3fv(checker.loc("col1"), 1, std::begin(rgb_to_array(page)));
+ glUniform3fv(checker.loc("col2"), 1, std::begin(checkerboard_darken(page)));
+ geom_to_uniform({1.0, 1.0}, checker.loc("subrect"));
+ for (auto &rect : pi.pages) {
+ set_page_transform(rect, checker);
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+ }
+
+ glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
+
+ // Desk
+ glUniform3fv(checker.loc("col1"), 1, std::begin(rgb_to_array(desk)));
+ glUniform3fv(checker.loc("col2"), 1, std::begin(checkerboard_darken(desk)));
+ geom_to_uniform(Geom::Scale(2.0, -2.0) * Geom::Translate(-1.0, 1.0), checker.loc("mat"), checker.loc("trans"));
+ geom_to_uniform({1.0, 1.0}, checker.loc("subrect"));
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
+
+ // Shadows
+ if (SP_RGBA32_A_U(border) != 0) {
+ auto dir = (Geom::Point(1.0, a.yaxisdir) * view.affine * Geom::Scale(1.0, -1.0)).normalized(); // Shadow direction rotates with view.
+ glUseProgram(shadow.id);
+ geom_to_uniform({1.0, 1.0}, shadow.loc("subrect"));
+ glUniform2fv(shadow.loc("wh"), 1, std::begin({(GLfloat)view.rect.width(), (GLfloat)view.rect.height()}));
+ glUniform1f(shadow.loc("size"), 40.0 * std::pow(std::abs(view.affine.det()), 0.25));
+ glUniform2fv(shadow.loc("dir"), 1, std::begin({(GLfloat)dir.x(), (GLfloat)dir.y()}));
+ glUniform4fv(shadow.loc("shadow_col"), 1, std::begin(premultiplied(rgba_to_array(border))));
+ for (auto &rect : pi.pages) {
+ set_page_transform(rect, shadow);
+ glDrawArrays(GL_TRIANGLES, 0, 3);
+ }
+ }
+
+ glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
+ }
+
+ glStencilFunc(GL_NOTEQUAL, 2, 2);
+
+ enum class DrawMode
+ {
+ Store,
+ Outline,
+ Combine
+ };
+
+ auto draw_store = [&, this] (Program const &prog, DrawMode drawmode) {
+ glUseProgram(prog.id);
+ geom_to_uniform({1.0, 1.0}, prog.loc("subrect"));
+ glUniform1i(prog.loc("tex"), drawmode == DrawMode::Outline ? 2 : 0);
+ if (drawmode == DrawMode::Combine) {
+ glUniform1i(prog.loc("tex_outline"), 2);
+ glUniform1f(prog.loc("opacity"), prefs.outline_overlay_opacity / 100.0);
+ }
+
+ if (stores.mode() == Stores::Mode::Normal) {
+ // Backing store fragment.
+ geom_to_uniform(calc_paste_transform(stores.store(), view) * Geom::Scale(1.0, -1.0), prog.loc("mat"), prog.loc("trans"));
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+ } else {
+ // Backing store fragment, clipped to its clean region.
+ geom_to_uniform(calc_paste_transform(stores.store(), view) * Geom::Scale(1.0, -1.0), prog.loc("mat"), prog.loc("trans"));
+ glBindVertexArray(clean_vao.vao);
+ glDrawArrays(GL_TRIANGLES, 0, 6 * clean_numrects);
+
+ // Snapshot fragment.
+ glUniform1i(prog.loc("tex"), drawmode == DrawMode::Outline ? 3 : 1);
+ if (drawmode == DrawMode::Combine) glUniform1i(prog.loc("tex_outline"), 3);
+ geom_to_uniform(calc_paste_transform(stores.snapshot(), view) * Geom::Scale(1.0, -1.0), prog.loc("mat"), prog.loc("trans"));
+ glBindVertexArray(rect.vao);
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+ }
+ };
+
+ if (a.splitmode == Inkscape::SplitMode::NORMAL || (a.splitmode == Inkscape::SplitMode::XRAY && !a.mouse)) {
+
+ // Drawing the backing store over the whole view.
+ a.render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY
+ ? draw_store(outlineoverlay, DrawMode::Combine)
+ : draw_store(texcopy, DrawMode::Store);
+
+ } else if (a.splitmode == Inkscape::SplitMode::SPLIT) {
+
+ // Calculate the clipping rectangles for split view.
+ auto [store_clip, outline_clip] = calc_splitview_cliprects(view.rect.dimensions(), a.splitfrac, a.splitdir);
+
+ glEnable(GL_SCISSOR_TEST);
+
+ // Draw the backing store.
+ glScissor(store_clip.left() * scale_factor, (view.rect.height() - store_clip.bottom()) * scale_factor, store_clip.width() * scale_factor, store_clip.height() * scale_factor);
+ a.render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY
+ ? draw_store(outlineoverlay, DrawMode::Combine)
+ : draw_store(texcopy, DrawMode::Store);
+
+ // Draw the outline store.
+ glScissor(outline_clip.left() * scale_factor, (view.rect.height() - outline_clip.bottom()) * scale_factor, outline_clip.width() * scale_factor, outline_clip.height() * scale_factor);
+ draw_store(texcopy, DrawMode::Outline);
+
+ glDisable(GL_SCISSOR_TEST);
+ glDisable(GL_STENCIL_TEST);
+
+ // Calculate the bounding rectangle of the split view controller.
+ auto rect = Geom::IntRect({0, 0}, view.rect.dimensions());
+ auto dim = a.splitdir == Inkscape::SplitDirection::EAST || a.splitdir == Inkscape::SplitDirection::WEST ? Geom::X : Geom::Y;
+ rect[dim] = Geom::IntInterval(-21, 21) + std::round(a.splitfrac[dim] * view.rect.dimensions()[dim]);
+
+ // Lease out a PixelStreamer mapping to draw on.
+ auto surface_size = rect.dimensions() * scale_factor;
+ auto surface = pixelstreamer->request(surface_size);
+ cairo_surface_set_device_scale(surface->cobj(), scale_factor, scale_factor);
+
+ // Actually draw the content with Cairo.
+ auto cr = Cairo::Context::create(surface);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source_rgba(0.0, 0.0, 0.0, 0.0);
+ cr->paint();
+ cr->translate(-rect.left(), -rect.top());
+ paint_splitview_controller(view.rect.dimensions(), a.splitfrac, a.splitdir, a.hoverdir, cr);
+
+ // Convert the surface to a texture.
+ glActiveTexture(GL_TEXTURE0);
+ auto texture = texturecache->request(surface_size);
+ pixelstreamer->finish(std::move(surface));
+
+ // Paint the texture onto the view.
+ glUseProgram(texcopy.id);
+ glUniform1i(texcopy.loc("tex"), 0);
+ geom_to_uniform(Geom::Scale(rect.dimensions()) * Geom::Translate(rect.min()) * Geom::Scale(2.0 / view.rect.width(), -2.0 / view.rect.height()) * Geom::Translate(-1.0, 1.0), texcopy.loc("mat"), texcopy.loc("trans"));
+ geom_to_uniform(Geom::Point(surface_size) / texture.size(), texcopy.loc("subrect"));
+ glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
+
+ // Return the texture back to the texture cache.
+ texturecache->finish(std::move(texture));
+
+ } else { // if (_split_mode == Inkscape::SplitMode::XRAY && a.mouse)
+
+ // Draw the backing store over the whole view.
+ auto const &shader = a.render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY ? outlineoverlayxray : xray;
+ glUseProgram(shader.id);
+ glUniform1f(shader.loc("radius"), prefs.xray_radius * scale_factor);
+ glUniform2fv(shader.loc("pos"), 1, std::begin({(GLfloat)(a.mouse->x() * scale_factor), (GLfloat)((view.rect.height() - a.mouse->y()) * scale_factor)}));
+ draw_store(shader, DrawMode::Combine);
+ }
+}
+
+} // 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 :
diff --git a/src/ui/widget/canvas/glgraphics.h b/src/ui/widget/canvas/glgraphics.h
new file mode 100644
index 0000000..7cb6ecf
--- /dev/null
+++ b/src/ui/widget/canvas/glgraphics.h
@@ -0,0 +1,144 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * OpenGL display backend.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_GLGRAPHICS_H
+#define INKSCAPE_UI_WIDGET_CANVAS_GLGRAPHICS_H
+
+#include <mutex>
+#include <epoxy/gl.h>
+#include "graphics.h"
+#include "texturecache.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class Stores;
+class Prefs;
+class PixelStreamer;
+
+template <GLuint type>
+struct Shader : boost::noncopyable
+{
+ GLuint id;
+ Shader(char const *src) { id = glCreateShader(type); glShaderSource(id, 1, &src, nullptr); glCompileShader(id); }
+ ~Shader() { glDeleteShader(id); }
+};
+using GShader = Shader<GL_GEOMETRY_SHADER>;
+using VShader = Shader<GL_VERTEX_SHADER>;
+using FShader = Shader<GL_FRAGMENT_SHADER>;
+
+struct Program : boost::noncopyable
+{
+ GLuint id = 0;
+ void create(VShader const &v, FShader const &f) { id = glCreateProgram(); glAttachShader(id, v.id); glAttachShader(id, f.id); glLinkProgram(id); }
+ void create(VShader const &v, const GShader &g, FShader const &f) { id = glCreateProgram(); glAttachShader(id, v.id); glAttachShader(id, g.id); glAttachShader(id, f.id); glLinkProgram(id); }
+ auto loc(char const *str) const { return glGetUniformLocation(id, str); }
+ ~Program() { glDeleteProgram(id); }
+};
+
+class VAO
+{
+public:
+ GLuint vao = 0;
+ GLuint vbuf;
+
+ VAO() = default;
+ VAO(GLuint vao, GLuint vbuf) : vao(vao), vbuf(vbuf) {}
+ VAO(VAO &&other) noexcept { movefrom(other); }
+ VAO &operator=(VAO &&other) noexcept { reset(); movefrom(other); return *this; }
+ ~VAO() { reset(); }
+
+private:
+ void reset() noexcept { if (vao) { glDeleteVertexArrays(1, &vao); glDeleteBuffers(1, &vbuf); } }
+ void movefrom(VAO &other) noexcept { vao = other.vao; vbuf = other.vbuf; other.vao = 0; }
+};
+
+struct GLFragment
+{
+ Texture texture;
+ Texture outline_texture;
+};
+
+class GLGraphics : public Graphics
+{
+public:
+ GLGraphics(Prefs const &prefs, Stores const &stores, PageInfo const &pi);
+ ~GLGraphics() override;
+
+ void set_scale_factor(int scale) override { scale_factor = scale; }
+ void set_outlines_enabled(bool) override;
+ void set_background_in_stores(bool enabled) override { background_in_stores = enabled; }
+ void set_colours(uint32_t p, uint32_t d, uint32_t b) override { page = p; desk = d; border = b; }
+
+ void recreate_store(Geom::IntPoint const &dimensions) override;
+ void shift_store(Fragment const &dest) override;
+ void swap_stores() override;
+ void fast_snapshot_combine() override;
+ void snapshot_combine(Fragment const &dest) override;
+ void invalidate_snapshot() override;
+
+ bool is_opengl() const override { return true; }
+ void invalidated_glstate() override { state = State::None; }
+
+ Cairo::RefPtr<Cairo::ImageSurface> request_tile_surface(Geom::IntRect const &rect, bool nogl) override;
+ void draw_tile(Fragment const &fragment, Cairo::RefPtr<Cairo::ImageSurface> surface, Cairo::RefPtr<Cairo::ImageSurface> outline_surface) override;
+ void junk_tile_surface(Cairo::RefPtr<Cairo::ImageSurface> surface) override;
+
+ void paint_widget(Fragment const &view, PaintArgs const &args, Cairo::RefPtr<Cairo::Context> const &cr) override;
+
+private:
+ // Drawn content.
+ GLFragment store, snapshot;
+
+ // OpenGL objects.
+ VAO rect; // Rectangle vertex data.
+ Program checker, shadow, texcopy, texcopydouble, outlineoverlay, xray, outlineoverlayxray; // Shaders
+ GLuint fbo; // Framebuffer object for rendering to stores.
+
+ // Pixel streamer and texture cache for uploading pixel data to GPU.
+ std::unique_ptr<PixelStreamer> pixelstreamer;
+ std::unique_ptr<TextureCache> texturecache;
+ std::mutex ps_mutex;
+
+ // For preventing unnecessary pipeline recreation.
+ enum class State { None, Widget, Stores, Tiles };
+ State state;
+ void setup_stores_pipeline();
+ void setup_tiles_pipeline();
+ void setup_widget_pipeline(Fragment const &view);
+
+ // For caching frequently-used uniforms.
+ GLuint mat_loc, trans_loc, subrect_loc, tex_loc, texoutline_loc;
+
+ // Dependency objects in canvas.
+ Prefs const &prefs;
+ Stores const &stores;
+ PageInfo const &pi;
+
+ // Backend-agnostic state.
+ int scale_factor = 1;
+ bool outlines_enabled = false;
+ bool background_in_stores = false;
+ uint32_t page, desk, border;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_GLGRAPHICS_H
+
+/*
+ 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 :
diff --git a/src/ui/widget/canvas/graphics.cpp b/src/ui/widget/canvas/graphics.cpp
new file mode 100644
index 0000000..28972e2
--- /dev/null
+++ b/src/ui/widget/canvas/graphics.cpp
@@ -0,0 +1,166 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <2geom/parallelogram.h>
+#include "ui/util.h"
+#include "helper/geom.h"
+#include "graphics.h"
+#include "util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+namespace {
+
+// Convert an rgba into a pattern, turning transparency into checkerboard-ness.
+Cairo::RefPtr<Cairo::Pattern> rgba_to_pattern(uint32_t rgba)
+{
+ if (SP_RGBA32_A_U(rgba) == 255) {
+ return Cairo::SolidPattern::create_rgb(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba));
+ } else {
+ int constexpr w = 6;
+ int constexpr h = 6;
+
+ auto dark = checkerboard_darken(rgba);
+
+ auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, 2 * w, 2 * h);
+
+ auto cr = Cairo::Context::create(surface);
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->set_source_rgb(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba), SP_RGBA32_B_F(rgba));
+ cr->paint();
+ cr->set_source_rgb(dark[0], dark[1], dark[2]);
+ cr->rectangle(0, 0, w, h);
+ cr->rectangle(w, h, w, h);
+ cr->fill();
+
+ auto pattern = Cairo::SurfacePattern::create(surface);
+ pattern->set_extend(Cairo::EXTEND_REPEAT);
+ pattern->set_filter(Cairo::FILTER_NEAREST);
+
+ return pattern;
+ }
+}
+
+} // namespace
+
+// Paint the background and pages using Cairo into the given fragment.
+void Graphics::paint_background(Fragment const &fragment, PageInfo const &pi, uint32_t page, uint32_t desk, Cairo::RefPtr<Cairo::Context> const &cr)
+{
+ cr->save();
+ cr->set_operator(Cairo::OPERATOR_SOURCE);
+ cr->rectangle(0, 0, fragment.rect.width(), fragment.rect.height());
+ cr->clip();
+
+ if (desk == page || check_single_page(fragment, pi)) {
+ // Desk and page are the same, or a single page fills the whole screen; just clear the fragment to page.
+ cr->set_source(rgba_to_pattern(page));
+ cr->paint();
+ } else {
+ // Paint the background to the complement of the pages. (Slightly overpaints when pages overlap.)
+ cr->save();
+ cr->set_source(rgba_to_pattern(desk));
+ cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD);
+ cr->rectangle(0, 0, fragment.rect.width(), fragment.rect.height());
+ cr->translate(-fragment.rect.left(), -fragment.rect.top());
+ cr->transform(geom_to_cairo(fragment.affine));
+ for (auto &rect : pi.pages) {
+ cr->rectangle(rect.left(), rect.top(), rect.width(), rect.height());
+ }
+ cr->fill();
+ cr->restore();
+
+ // Paint the pages.
+ cr->save();
+ cr->set_source(rgba_to_pattern(page));
+ cr->translate(-fragment.rect.left(), -fragment.rect.top());
+ cr->transform(geom_to_cairo(fragment.affine));
+ for (auto &rect : pi.pages) {
+ cr->rectangle(rect.left(), rect.top(), rect.width(), rect.height());
+ }
+ cr->fill();
+ cr->restore();
+ }
+
+ cr->restore();
+}
+
+std::pair<Geom::IntRect, Geom::IntRect> Graphics::calc_splitview_cliprects(Geom::IntPoint const &size, Geom::Point const &split_frac, SplitDirection split_direction)
+{
+ auto window = Geom::IntRect({0, 0}, size);
+
+ auto content = window;
+ auto outline = window;
+ auto split = [&] (Geom::Dim2 dim, Geom::IntRect &lo, Geom::IntRect &hi) {
+ int s = std::round(split_frac[dim] * size[dim]);
+ lo[dim].setMax(s);
+ hi[dim].setMin(s);
+ };
+
+ switch (split_direction) {
+ case Inkscape::SplitDirection::NORTH: split(Geom::Y, content, outline); break;
+ case Inkscape::SplitDirection::EAST: split(Geom::X, outline, content); break;
+ case Inkscape::SplitDirection::SOUTH: split(Geom::Y, outline, content); break;
+ case Inkscape::SplitDirection::WEST: split(Geom::X, content, outline); break;
+ default: assert(false); break;
+ }
+
+ return std::make_pair(content, outline);
+}
+
+void Graphics::paint_splitview_controller(Geom::IntPoint const &size, Geom::Point const &split_frac, SplitDirection split_direction, SplitDirection hover_direction, Cairo::RefPtr<Cairo::Context> const &cr)
+{
+ auto split_position = (split_frac * size).round();
+
+ // Add dividing line.
+ cr->set_source_rgb(0.0, 0.0, 0.0);
+ cr->set_line_width(1.0);
+ if (split_direction == Inkscape::SplitDirection::EAST ||
+ split_direction == Inkscape::SplitDirection::WEST) {
+ cr->move_to(split_position.x() + 0.5, 0.0 );
+ cr->line_to(split_position.x() + 0.5, size.y());
+ cr->stroke();
+ } else {
+ cr->move_to(0.0 , split_position.y() + 0.5);
+ cr->line_to(size.x(), split_position.y() + 0.5);
+ cr->stroke();
+ }
+
+ // Add controller image.
+ double a = hover_direction == Inkscape::SplitDirection::NONE ? 0.5 : 1.0;
+ cr->set_source_rgba(0.2, 0.2, 0.2, a);
+ cr->arc(split_position.x(), split_position.y(), 20, 0, 2 * M_PI);
+ cr->fill();
+
+ for (int i = 0; i < 4; i++) {
+ // The four direction triangles.
+ cr->save();
+
+ // Position triangle.
+ cr->translate(split_position.x(), split_position.y());
+ cr->rotate((i + 2) * M_PI / 2);
+
+ // Draw triangle.
+ cr->move_to(-5, 8);
+ cr->line_to( 0, 18);
+ cr->line_to( 5, 8);
+ cr->close_path();
+
+ double b = (int)hover_direction == (i + 1) ? 0.9 : 0.7;
+ cr->set_source_rgba(b, b, b, a);
+ cr->fill();
+
+ cr->restore();
+ }
+}
+
+bool Graphics::check_single_page(Fragment const &view, PageInfo const &pi)
+{
+ auto pl = Geom::Parallelogram(view.rect) * view.affine.inverse();
+ return std::any_of(pi.pages.begin(), pi.pages.end(), [&] (auto &rect) {
+ return Geom::Parallelogram(rect).contains(pl);
+ });
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
diff --git a/src/ui/widget/canvas/graphics.h b/src/ui/widget/canvas/graphics.h
new file mode 100644
index 0000000..0e7767d
--- /dev/null
+++ b/src/ui/widget/canvas/graphics.h
@@ -0,0 +1,91 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Display backend interface.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_GRAPHICS_H
+#define INKSCAPE_UI_WIDGET_CANVAS_GRAPHICS_H
+
+#include <memory>
+#include <cstdint>
+#include <boost/noncopyable.hpp>
+#include <2geom/rect.h>
+#include <cairomm/cairomm.h>
+#include "display/rendermode.h"
+#include "fragment.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+class Stores;
+class Prefs;
+
+struct PageInfo
+{
+ std::vector<Geom::Rect> pages;
+};
+
+class Graphics
+{
+public:
+ // Creation/destruction.
+ static std::unique_ptr<Graphics> create_gl (Prefs const &prefs, Stores const &stores, PageInfo const &pi);
+ static std::unique_ptr<Graphics> create_cairo(Prefs const &prefs, Stores const &stores, PageInfo const &pi);
+ virtual ~Graphics() = default;
+
+ // State updating.
+ virtual void set_scale_factor(int) = 0; ///< Set the HiDPI scale factor.
+ virtual void set_outlines_enabled(bool) = 0; ///< Whether to maintain a second layer of outline content.
+ virtual void set_background_in_stores(bool) = 0; ///< Whether to assume the first layer is drawn on top of background or transparency.
+ virtual void set_colours(uint32_t page, uint32_t desk, uint32_t border) = 0; ///< Set colours for background/page shadow drawing.
+
+ // Store manipulation.
+ virtual void recreate_store(Geom::IntPoint const &dims) = 0; ///< Set the store to a surface of the given size, of unspecified contents.
+ virtual void shift_store(Fragment const &dest) = 0; ///< Called when the store fragment shifts position to \a dest.
+ virtual void swap_stores() = 0; ///< Exchange the store and snapshot surfaces.
+ virtual void fast_snapshot_combine() = 0; ///< Paste the store onto the snapshot.
+ virtual void snapshot_combine(Fragment const &dest) = 0; ///< Paste the snapshot followed by the store onto a new snapshot at \a dest.
+ virtual void invalidate_snapshot() = 0; ///< Indicate that the content in the snapshot store is not going to be used again.
+
+ // Misc.
+ virtual bool is_opengl() const = 0; ///< Whether this is an OpenGL backend.
+ virtual void invalidated_glstate() = 0; ///< Tells the Graphics to no longer rely on any OpenGL state it had set up.
+
+ // Tile drawing.
+ /// Return a surface for drawing on. If nogl is true, no GL commands are issued, as is a requirement off-main-thread. All such surfaces must be
+ /// returned by passing them either to draw_tile() or junk_tile_surface().
+ virtual Cairo::RefPtr<Cairo::ImageSurface> request_tile_surface(Geom::IntRect const &rect, bool nogl) = 0;
+ /// Commit the contents of a surface previously issued by request_tile_surface() to the canvas. In outline mode, a second surface must be passed
+ /// containing the outline content, otherwise it should be null.
+ virtual void draw_tile(Fragment const &fragment, Cairo::RefPtr<Cairo::ImageSurface> surface, Cairo::RefPtr<Cairo::ImageSurface> outline_surface) = 0;
+ /// Get rid of a surface previously issued by request_tile_surface() without committing it to the canvas. Usually useful only to dispose of
+ /// surfaces which have gone into an error state while rendering, which is irreversible, and therefore we can't do anything useful with them.
+ virtual void junk_tile_surface(Cairo::RefPtr<Cairo::ImageSurface> surface) = 0;
+
+ // Widget painting.
+ struct PaintArgs
+ {
+ std::optional<Geom::IntPoint> mouse;
+ RenderMode render_mode;
+ SplitMode splitmode;
+ Geom::Point splitfrac;
+ SplitDirection splitdir;
+ SplitDirection hoverdir;
+ double yaxisdir;
+ };
+ virtual void paint_widget(Fragment const &view, PaintArgs const &args, Cairo::RefPtr<Cairo::Context> const &cr) = 0;
+
+ // Static functions providing common functionality.
+ static bool check_single_page(Fragment const &view, PageInfo const &pi);
+ static std::pair<Geom::IntRect, Geom::IntRect> calc_splitview_cliprects(Geom::IntPoint const &size, Geom::Point const &splitfrac, SplitDirection splitdir);
+ static void paint_splitview_controller(Geom::IntPoint const &size, Geom::Point const &splitfrac, SplitDirection splitdir, SplitDirection hoverdir, Cairo::RefPtr<Cairo::Context> const &cr);
+ static void paint_background(Fragment const &fragment, PageInfo const &pi, uint32_t page, uint32_t desk, Cairo::RefPtr<Cairo::Context> const &cr);
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_GRAPHICS_H
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 :
diff --git a/src/ui/widget/canvas/pixelstreamer.h b/src/ui/widget/canvas/pixelstreamer.h
new file mode 100644
index 0000000..bcd3684
--- /dev/null
+++ b/src/ui/widget/canvas/pixelstreamer.h
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * A class hierarchy implementing various ways of streaming pixel buffers to the GPU.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_PIXELSTREAMER_H
+#define INKSCAPE_UI_WIDGET_CANVAS_PIXELSTREAMER_H
+
+#include <memory>
+#include <2geom/int-point.h>
+#include <cairomm/refptr.h>
+#include <cairomm/surface.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+// A class for turning Cairo image surfaces into OpenGL textures.
+class PixelStreamer
+{
+public:
+ virtual ~PixelStreamer() = default;
+
+ // Method for streaming pixels to the GPU.
+ enum class Method
+ {
+ Auto, // Use the best option available at runtime.
+ Persistent, // Persistent buffer mapping. (Best, requires OpenGL 4.4.)
+ Asynchronous, // Ordinary buffer mapping. (Almost as good, requires OpenGL 3.0.)
+ Synchronous // Synchronous texture uploads. (Worst but still tolerable, requires OpenGL 1.1.)
+ };
+
+ // Create a PixelStreamer using a choice of method specified at runtime, falling back if unsupported.
+ static std::unique_ptr<PixelStreamer> create_supported(Method method);
+
+ // Return the method in use.
+ virtual Method get_method() const = 0;
+
+ /**
+ * Request a drawing surface of the given dimensions. If nogl is true, no GL commands will be issued,
+ * but the request may fail. An effort is made to keep such failures to a minimum.
+ *
+ * The surface must be returned to the PixelStreamer by calling finish(), in order to deallocate
+ * GL resourecs.
+ */
+ virtual Cairo::RefPtr<Cairo::ImageSurface> request(Geom::IntPoint const &dimensions, bool nogl = false) = 0;
+
+ /**
+ * Give back a drawing surface produced by request(), uploading the contents to the currently bound texture.
+ * The texture must be at least as big as the surface.
+ *
+ * If junk is true, then the surface will be junked instead, meaning nothing will be done with the contents,
+ * and its GL resources will simply be deallocated.
+ */
+ virtual void finish(Cairo::RefPtr<Cairo::ImageSurface> surface, bool junk = false) = 0;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_PIXELSTREAMER_H
+
+/*
+ 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 :
diff --git a/src/ui/widget/canvas/prefs.h b/src/ui/widget/canvas/prefs.h
new file mode 100644
index 0000000..363fb6d
--- /dev/null
+++ b/src/ui/widget/canvas/prefs.h
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_PREFS_H
+#define INKSCAPE_UI_WIDGET_CANVAS_PREFS_H
+
+#include "preferences.h"
+
+namespace Inkscape::UI::Widget {
+
+class Prefs
+{
+public:
+ Prefs()
+ {
+ devmode.action = [this] { set_devmode(devmode); };
+ devmode.action();
+ }
+
+ // Main preferences
+ Pref<int> xray_radius = { "/options/rendering/xray-radius", 100, 1, 1500 };
+ Pref<int> outline_overlay_opacity = { "/options/rendering/outline-overlay-opacity", 50, 0, 100 };
+ Pref<int> update_strategy = { "/options/rendering/update_strategy", 3, 1, 3 };
+ Pref<bool> request_opengl = { "/options/rendering/request_opengl" };
+ Pref<int> grabsize = { "/options/grabsize/value", 3, 1, 15 };
+ Pref<int> numthreads = { "/options/threading/numthreads", 0, 1, 256 };
+
+ // Colour management
+ Pref<bool> from_display = { "/options/displayprofile/from_display" };
+ Pref<void> displayprofile = { "/options/displayprofile" };
+ Pref<void> softproof = { "/options/softproof" };
+
+ // Auto-scrolling
+ Pref<int> autoscrolldistance = { "/options/autoscrolldistance/value", 0, -1000, 10000 };
+ Pref<double> autoscrollspeed = { "/options/autoscrollspeed/value", 1.0, 0.0, 10.0 };
+
+ // Devmode preferences
+ Pref<int> tile_size = { "/options/rendering/tile_size", 300, 1, 10000 };
+ Pref<int> render_time_limit = { "/options/rendering/render_time_limit", 80, 1, 5000 };
+ Pref<bool> block_updates = { "/options/rendering/block_updates", true };
+ Pref<int> pixelstreamer_method = { "/options/rendering/pixelstreamer_method", 1, 1, 4 };
+ Pref<int> padding = { "/options/rendering/padding", 350, 0, 1000 };
+ Pref<int> prerender = { "/options/rendering/prerender", 100, 0, 1000 };
+ Pref<int> preempt = { "/options/rendering/preempt", 250, 0, 1000 };
+ Pref<int> coarsener_min_size = { "/options/rendering/coarsener_min_size", 200, 0, 1000 };
+ Pref<int> coarsener_glue_size = { "/options/rendering/coarsener_glue_size", 80, 0, 1000 };
+ Pref<double> coarsener_min_fullness = { "/options/rendering/coarsener_min_fullness", 0.3, 0.0, 1.0 };
+
+ // Debug switches
+ Pref<bool> debug_framecheck = { "/options/rendering/debug_framecheck" };
+ Pref<bool> debug_logging = { "/options/rendering/debug_logging" };
+ Pref<bool> debug_delay_redraw = { "/options/rendering/debug_delay_redraw" };
+ Pref<int> debug_delay_redraw_time = { "/options/rendering/debug_delay_redraw_time", 50, 0, 1000000 };
+ Pref<bool> debug_show_redraw = { "/options/rendering/debug_show_redraw" };
+ Pref<bool> debug_show_unclean = { "/options/rendering/debug_show_unclean" }; // no longer implemented
+ Pref<bool> debug_show_snapshot = { "/options/rendering/debug_show_snapshot" };
+ Pref<bool> debug_show_clean = { "/options/rendering/debug_show_clean" }; // no longer implemented
+ Pref<bool> debug_disable_redraw = { "/options/rendering/debug_disable_redraw" };
+ Pref<bool> debug_sticky_decoupled = { "/options/rendering/debug_sticky_decoupled" };
+ Pref<bool> debug_animate = { "/options/rendering/debug_animate" };
+
+private:
+ // Developer mode
+ Pref<bool> devmode = { "/options/rendering/devmode" };
+
+ void set_devmode(bool on)
+ {
+ tile_size.set_enabled(on);
+ render_time_limit.set_enabled(on);
+ pixelstreamer_method.set_enabled(on);
+ padding.set_enabled(on);
+ prerender.set_enabled(on);
+ preempt.set_enabled(on);
+ coarsener_min_size.set_enabled(on);
+ coarsener_glue_size.set_enabled(on);
+ coarsener_min_fullness.set_enabled(on);
+ debug_framecheck.set_enabled(on);
+ debug_logging.set_enabled(on);
+ debug_delay_redraw.set_enabled(on);
+ debug_delay_redraw_time.set_enabled(on);
+ debug_show_redraw.set_enabled(on);
+ debug_show_unclean.set_enabled(on);
+ debug_show_snapshot.set_enabled(on);
+ debug_show_clean.set_enabled(on);
+ debug_disable_redraw.set_enabled(on);
+ debug_sticky_decoupled.set_enabled(on);
+ debug_animate.set_enabled(on);
+ }
+};
+
+} // namespace Inkscape::UI::Widget
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_PREFS_H
+
+/*
+ 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 :
diff --git a/src/ui/widget/canvas/stores.cpp b/src/ui/widget/canvas/stores.cpp
new file mode 100644
index 0000000..70327f5
--- /dev/null
+++ b/src/ui/widget/canvas/stores.cpp
@@ -0,0 +1,371 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <array>
+#include <cmath>
+#include <2geom/transforms.h>
+#include <2geom/parallelogram.h>
+#include <2geom/point.h>
+#include "helper/geom.h"
+#include "ui/util.h"
+#include "stores.h"
+#include "prefs.h"
+#include "fragment.h"
+#include "graphics.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+namespace {
+
+// Determine whether an affine transformation approximately maps the unit square [0, 1]^2 to itself.
+bool preserves_unitsquare(Geom::Affine const &affine)
+{
+ return approx_dihedral(Geom::Translate(0.5, 0.5) * affine * Geom::Translate(-0.5, -0.5));
+}
+
+// Apply an affine transformation to a region, then return a strictly smaller region approximating it, made from chunks of size roughly d.
+// To reduce computation, only the intersection of the result with bounds will be valid.
+auto region_affine_approxinwards(Cairo::RefPtr<Cairo::Region> const &reg, Geom::Affine const &affine, Geom::IntRect const &bounds, int d = 200)
+{
+ // Trivial empty case.
+ if (reg->empty()) return Cairo::Region::create();
+
+ // Trivial identity case.
+ if (affine.isIdentity(0.001)) return reg->copy();
+
+ // Fast-path for rectilinear transformations.
+ if (affine.withoutTranslation().isScale(0.001)) {
+ auto regdst = Cairo::Region::create();
+
+ auto transform = [&] (const Geom::IntPoint &p) {
+ return (Geom::Point(p) * affine).round();
+ };
+
+ for (int i = 0; i < reg->get_num_rectangles(); i++) {
+ auto rect = cairo_to_geom(reg->get_rectangle(i));
+ regdst->do_union(geom_to_cairo(Geom::IntRect(transform(rect.min()), transform(rect.max()))));
+ }
+
+ return regdst;
+ }
+
+ // General case.
+ auto ext = cairo_to_geom(reg->get_extents());
+ auto rectdst = ((Geom::Parallelogram(ext) * affine).bounds().roundOutwards() & bounds).regularized();
+ if (!rectdst) return Cairo::Region::create();
+ auto rectsrc = (Geom::Parallelogram(*rectdst) * affine.inverse()).bounds().roundOutwards();
+
+ auto regdst = Cairo::Region::create(geom_to_cairo(*rectdst));
+ auto regsrc = Cairo::Region::create(geom_to_cairo(rectsrc));
+ regsrc->subtract(reg);
+
+ double fx = min(absolute(Geom::Point(1.0, 0.0) * affine.withoutTranslation()));
+ double fy = min(absolute(Geom::Point(0.0, 1.0) * affine.withoutTranslation()));
+
+ for (int i = 0; i < regsrc->get_num_rectangles(); i++)
+ {
+ auto rect = cairo_to_geom(regsrc->get_rectangle(i));
+ int nx = std::ceil(rect.width() * fx / d);
+ int ny = std::ceil(rect.height() * fy / d);
+ auto pt = [&] (int x, int y) {
+ return rect.min() + (rect.dimensions() * Geom::IntPoint(x, y)) / Geom::IntPoint(nx, ny);
+ };
+ for (int x = 0; x < nx; x++) {
+ for (int y = 0; y < ny; y++) {
+ auto r = Geom::IntRect(pt(x, y), pt(x + 1, y + 1));
+ auto r2 = (Geom::Parallelogram(r) * affine).bounds().roundOutwards();
+ regdst->subtract(geom_to_cairo(r2));
+ }
+ }
+ }
+
+ return regdst;
+}
+
+} // namespace
+
+Geom::IntRect Stores::centered(Fragment const &view) const
+{
+ // Return the visible region of the view, plus the prerender and padding margins.
+ return expandedBy(view.rect, _prefs.prerender + _prefs.padding);
+}
+
+void Stores::recreate_store(Fragment const &view)
+{
+ // Recreate the store at the view's affine.
+ _store.affine = view.affine;
+ _store.rect = centered(view);
+ _store.drawn = Cairo::Region::create();
+ // Tell the graphics to create a blank new store.
+ _graphics->recreate_store(_store.rect.dimensions());
+}
+
+void Stores::shift_store(Fragment const &view)
+{
+ // Create a new fragment centred on the viewport.
+ auto rect = centered(view);
+ // Tell the graphics to copy the drawn part of the old store to the new store.
+ _graphics->shift_store(Fragment{ _store.affine, rect });
+ // Set the shifted store as the new store.
+ _store.rect = rect;
+ // Clip the drawn region to the new store.
+ _store.drawn->intersect(geom_to_cairo(_store.rect));
+};
+
+void Stores::take_snapshot(Fragment const &view)
+{
+ // Copy the store to the snapshot, leaving us temporarily in an invalid state.
+ _snapshot = std::move(_store);
+ // Tell the graphics to do the same, except swapping them so we can re-use the old snapshot store.
+ _graphics->swap_stores();
+ // Reset the store.
+ recreate_store(view);
+ // Transform the snapshot's drawn region to the new store's affine.
+ _snapshot.drawn = shrink_region(region_affine_approxinwards(_snapshot.drawn, _snapshot.affine.inverse() * _store.affine, _store.rect), 4, -2);
+}
+
+void Stores::snapshot_combine(Fragment const &view)
+{
+ // Add the drawn region to the snapshot drawn region (they both exist in store space, so this is valid), and save its affine.
+ _snapshot.drawn->do_union(_store.drawn);
+ auto old_store_affine = _store.affine;
+
+ // Get the list of corner points in the store's drawn region and the snapshot bounds rect, all at the view's affine.
+ std::vector<Geom::Point> pts;
+ auto add_rect = [&, this] (Geom::Parallelogram const &pl) {
+ for (int i = 0; i < 4; i++) {
+ pts.emplace_back(Geom::Point(pl.corner(i)));
+ }
+ };
+ auto add_store = [&, this] (Store const &s) {
+ int nrects = s.drawn->get_num_rectangles();
+ auto affine = s.affine.inverse() * view.affine;
+ for (int i = 0; i < nrects; i++) {
+ add_rect(Geom::Parallelogram(cairo_to_geom(s.drawn->get_rectangle(i))) * affine);
+ }
+ };
+ add_store(_store);
+ add_rect(Geom::Parallelogram(_snapshot.rect) * _snapshot.affine.inverse() * view.affine);
+
+ // Compute their minimum-area bounding box as a fragment - an (affine, rect) pair.
+ auto [affine, rect] = min_bounding_box(pts);
+ affine = view.affine * affine;
+
+ // Check if the paste transform takes the snapshot store exactly onto the new fragment, possibly with a dihedral transformation.
+ auto paste = Geom::Scale(_snapshot.rect.dimensions())
+ * Geom::Translate(_snapshot.rect.min())
+ * _snapshot.affine.inverse()
+ * affine
+ * Geom::Translate(-rect.min())
+ * Geom::Scale(rect.dimensions()).inverse();
+ if (preserves_unitsquare(paste)) {
+ // If so, simply take the new fragment to be exactly the same as the snapshot store.
+ rect = _snapshot.rect;
+ affine = _snapshot.affine;
+ }
+
+ // Compute the scale difference between the backing store and the new fragment, giving the amount of detail that would be lost by pasting.
+ if ( double scale_ratio = std::sqrt(std::abs(_store.affine.det() / affine.det()));
+ scale_ratio > 4.0 )
+ {
+ // Zoom the new fragment in to increase its quality.
+ double grow = scale_ratio / 2.0;
+ rect *= Geom::Scale(grow);
+ affine *= Geom::Scale(grow);
+ }
+
+ // Do not allow the fragment to become more detailed than the window.
+ if ( double scale_ratio = std::sqrt(std::abs(affine.det() / view.affine.det()));
+ scale_ratio > 1.0 )
+ {
+ // Zoom the new fragment out to reduce its quality.
+ double shrink = 1.0 / scale_ratio;
+ rect *= Geom::Scale(shrink);
+ affine *= Geom::Scale(shrink);
+ }
+
+ // Find the bounding rect of the visible region + prerender margin within the new fragment. We do not want to discard this content in the next clipping step.
+ auto renderable = (Geom::Parallelogram(expandedBy(view.rect, _prefs.prerender)) * view.affine.inverse() * affine).bounds() & rect;
+
+ // Cap the dimensions of the new fragment to slightly larger than the maximum dimension of the window by clipping it towards the screen centre. (Lower in Cairo mode since otherwise too slow to cope.)
+ double max_dimension = max(view.rect.dimensions()) * (_graphics->is_opengl() ? 1.7 : 0.8);
+ auto dimens = rect.dimensions();
+ dimens.x() = std::min(dimens.x(), max_dimension);
+ dimens.y() = std::min(dimens.y(), max_dimension);
+ auto center = Geom::Rect(view.rect).midpoint() * view.affine.inverse() * affine;
+ center.x() = Util::safeclamp(center.x(), rect.left() + dimens.x() * 0.5, rect.right() - dimens.x() * 0.5);
+ center.y() = Util::safeclamp(center.y(), rect.top() + dimens.y() * 0.5, rect.bottom() - dimens.y() * 0.5);
+ rect = Geom::Rect(center - dimens * 0.5, center + dimens * 0.5);
+
+ // Ensure the new fragment contains the renderable rect from earlier, enlarging it and reducing resolution if necessary.
+ if (!rect.contains(renderable)) {
+ auto oldrect = rect;
+ rect.unionWith(renderable);
+ double shrink = 1.0 / std::max(rect.width() / oldrect.width(), rect.height() / oldrect.height());
+ rect *= Geom::Scale(shrink);
+ affine *= Geom::Scale(shrink);
+ }
+
+ // Calculate the paste transform from the snapshot store to the new fragment (again).
+ paste = Geom::Scale(_snapshot.rect.dimensions())
+ * Geom::Translate(_snapshot.rect.min())
+ * _snapshot.affine.inverse()
+ * affine
+ * Geom::Translate(-rect.min())
+ * Geom::Scale(rect.dimensions()).inverse();
+
+ if (_prefs.debug_logging) std::cout << "New fragment dimensions " << rect.width() << ' ' << rect.height() << std::endl;
+
+ if (paste.isIdentity(0.001) && rect.dimensions().round() == _snapshot.rect.dimensions()) {
+ // Fast path: simply paste the backing store onto the snapshot store.
+ if (_prefs.debug_logging) std::cout << "Fast snapshot combine" << std::endl;
+ _graphics->fast_snapshot_combine();
+ } else {
+ // General path: paste the snapshot store and then the backing store onto a new fragment, then set that as the snapshot store.
+ auto frag_rect = rect.roundOutwards();
+ _graphics->snapshot_combine(Fragment{ affine, frag_rect });
+ _snapshot.rect = frag_rect;
+ _snapshot.affine = affine;
+ }
+
+ // Start drawing again on a new blank store aligned to the screen.
+ recreate_store(view);
+ // Transform the snapshot clean region to the new store.
+ // Todo: Should really clip this to the new snapshot rect, only we can't because it's generally not aligned with the store's affine.
+ _snapshot.drawn = shrink_region(region_affine_approxinwards(_snapshot.drawn, old_store_affine.inverse() * _store.affine, _store.rect), 4, -2);
+};
+
+void Stores::reset()
+{
+ _mode = Mode::None;
+ _store.drawn.clear();
+ _snapshot.drawn.clear();
+}
+
+// Handle transitions and actions in response to viewport changes.
+auto Stores::update(Fragment const &view) -> Action
+{
+ switch (_mode) {
+
+ case Mode::None: {
+ // Not yet initialised or just reset - create store for first time.
+ recreate_store(view);
+ _mode = Mode::Normal;
+ if (_prefs.debug_logging) std::cout << "Full reset" << std::endl;
+ return Action::Recreated;
+ }
+
+ case Mode::Normal: {
+ auto result = Action::None;
+ // Enter decoupled mode if the affine has changed from what the store was drawn at.
+ if (view.affine != _store.affine) {
+ // Snapshot and reset the store.
+ take_snapshot(view);
+ // Enter decoupled mode.
+ _mode = Mode::Decoupled;
+ if (_prefs.debug_logging) std::cout << "Enter decoupled mode" << std::endl;
+ result = Action::Recreated;
+ } else {
+ // Determine whether the view has moved sufficiently far that we need to shift the store.
+ if (!_store.rect.contains(expandedBy(view.rect, _prefs.prerender))) {
+ // The visible region + prerender margin has reached the edge of the store.
+ if (!(cairo_to_geom(_store.drawn->get_extents()) & expandedBy(view.rect, _prefs.prerender + _prefs.padding)).regularized()) {
+ // If the store contains no reusable content at all, recreate it.
+ recreate_store(view);
+ if (_prefs.debug_logging) std::cout << "Recreate store" << std::endl;
+ result = Action::Recreated;
+ } else {
+ // Otherwise shift it.
+ shift_store(view);
+ if (_prefs.debug_logging) std::cout << "Shift store" << std::endl;
+ result = Action::Shifted;
+ }
+ }
+ }
+ // After these operations, the store should now contain the visible region + prerender margin.
+ assert(_store.rect.contains(expandedBy(view.rect, _prefs.prerender)));
+ return result;
+ }
+
+ case Mode::Decoupled: {
+ // Completely cancel the previous redraw and start again if the viewing parameters have changed too much.
+ auto check_restart_redraw = [&, this] {
+ // With this debug feature on, redraws should never be restarted.
+ if (_prefs.debug_sticky_decoupled) return false;
+
+ // Restart if the store is no longer covering the middle 50% of the screen. (Usually triggered by rotating or zooming out.)
+ auto pl = Geom::Parallelogram(view.rect);
+ pl *= Geom::Translate(-pl.midpoint()) * Geom::Scale(0.5) * Geom::Translate(pl.midpoint());
+ pl *= view.affine.inverse() * _store.affine;
+ if (!Geom::Parallelogram(_store.rect).contains(pl)) {
+ if (_prefs.debug_logging) std::cout << "Restart redraw (store not fully covering screen)" << std::endl;
+ return true;
+ }
+
+ // Also restart if zoomed in or out too much.
+ auto scale_ratio = std::abs(view.affine.det() / _store.affine.det());
+ if (scale_ratio > 3.0 || scale_ratio < 0.7) {
+ // Todo: Un-hard-code these thresholds.
+ // * The threshold 3.0 is for zooming in. It says that if the quality of what is being redrawn is more than 3x worse than that of the screen, restart. This is necessary to ensure acceptably high resolution is kept as you zoom in.
+ // * The threshold 0.7 is for zooming out. It says that if the quality of what is being redrawn is too high compared to the screen, restart. This prevents wasting time redrawing the screen slowly, at too high a quality that will probably not ever be seen.
+ if (_prefs.debug_logging) std::cout << "Restart redraw (zoomed changed too much)" << std::endl;
+ return true;
+ }
+
+ // Don't restart.
+ return false;
+ };
+
+ if (check_restart_redraw()) {
+ // Re-use as much content as possible from the store and the snapshot, and set as the new snapshot.
+ snapshot_combine(view);
+ return Action::Recreated;
+ }
+
+ return Action::None;
+ }
+
+ default: {
+ assert(false);
+ return Action::None;
+ }
+ }
+}
+
+auto Stores::finished_draw(Fragment const &view) -> Action
+{
+ // Finished drawing. Handle transitions out of decoupled mode, by checking if we need to reset the store to the correct affine.
+ if (_mode == Mode::Decoupled) {
+ if (_prefs.debug_sticky_decoupled) {
+ // Debug feature: stop redrawing, but stay in decoupled mode.
+ } else if (_store.affine == view.affine) {
+ // Store is at the correct affine - exit decoupled mode.
+ if (_prefs.debug_logging) std::cout << "Exit decoupled mode" << std::endl;
+ // Exit decoupled mode.
+ _mode = Mode::Normal;
+ _graphics->invalidate_snapshot();
+ } else {
+ // Content is rendered at the wrong affine - take a new snapshot and continue idle process to continue rendering at the new affine.
+ // Snapshot and reset the backing store.
+ take_snapshot(view);
+ if (_prefs.debug_logging) std::cout << "Remain in decoupled mode" << std::endl;
+ return Action::Recreated;
+ }
+ }
+
+ return Action::None;
+}
+
+} // 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 :
diff --git a/src/ui/widget/canvas/stores.h b/src/ui/widget/canvas/stores.h
new file mode 100644
index 0000000..70b10cc
--- /dev/null
+++ b/src/ui/widget/canvas/stores.h
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Abstraction of the store/snapshot mechanism.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_STORES_H
+#define INKSCAPE_UI_WIDGET_CANVAS_STORES_H
+
+#include "fragment.h"
+#include "util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+struct Fragment;
+class Prefs;
+class Graphics;
+
+class Stores
+{
+public:
+ enum class Mode
+ {
+ None, /// Not initialised or just reset; no stores exist yet.
+ Normal, /// Normal mode consisting of just a backing store.
+ Decoupled /// Decoupled mode consisting of both a backing store and a snapshot store.
+ };
+
+ enum class Action
+ {
+ None, /// The backing store was not changed.
+ Recreated, /// The backing store was completely recreated.
+ Shifted /// The backing store was shifted into a new rectangle.
+ };
+
+ struct Store : Fragment
+ {
+ /**
+ * The region of space containing drawn content.
+ * For the snapshot, this region is transformed to store space and approximated inwards.
+ */
+ Cairo::RefPtr<Cairo::Region> drawn;
+ };
+
+ /// Construct a blank object with no stores.
+ Stores(Prefs const &prefs)
+ : _mode(Mode::None)
+ , _graphics(nullptr)
+ , _prefs(prefs) {}
+
+ /// Set the pointer to the graphics object.
+ void set_graphics(Graphics *g) { _graphics = g; }
+
+ /// Discards all stores. (The actual operation on the graphics is performed on the next update().)
+ void reset();
+
+ /// Respond to a viewport change. (Requires a valid graphics.)
+ Action update(Fragment const &view);
+
+ /// Respond to drawing of the backing store having finished. (Requires a valid graphics.)
+ Action finished_draw(Fragment const &view);
+
+ /// Record a rectangle as being drawn to the store.
+ void mark_drawn(Geom::IntRect const &rect) { _store.drawn->do_union(geom_to_cairo(rect)); }
+
+ // Getters.
+ Store const &store() const { return _store; }
+ Store const &snapshot() const { return _snapshot; }
+ Mode mode() const { return _mode; }
+
+private:
+ // Internal state.
+ Mode _mode;
+ Store _store, _snapshot;
+
+ // The graphics object that executes the operations on the stores.
+ Graphics *_graphics;
+
+ // The preferences object we read preferences from.
+ Prefs const &_prefs;
+
+ // Internal actions.
+ Geom::IntRect centered(Fragment const &view) const;
+ void recreate_store(Fragment const &view);
+ void shift_store(Fragment const &view);
+ void take_snapshot(Fragment const &view);
+ void snapshot_combine(Fragment const &view);
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_STORES_H
+
+/*
+ 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 :
diff --git a/src/ui/widget/canvas/synchronizer.cpp b/src/ui/widget/canvas/synchronizer.cpp
new file mode 100644
index 0000000..331057b
--- /dev/null
+++ b/src/ui/widget/canvas/synchronizer.cpp
@@ -0,0 +1,103 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "synchronizer.h"
+#include <cassert>
+
+namespace Inkscape::UI::Widget {
+
+Synchronizer::Synchronizer()
+{
+ dispatcher.connect([this] { on_dispatcher(); });
+}
+
+void Synchronizer::signalExit() const
+{
+ auto lock = std::unique_lock(mutables);
+ awaken();
+ assert(slots.empty());
+ exitposted = true;
+}
+
+void Synchronizer::runInMain(std::function<void()> const &f) const
+{
+ auto lock = std::unique_lock(mutables);
+ awaken();
+ auto s = Slot{ &f };
+ slots.emplace_back(&s);
+ assert(!exitposted);
+ slots_cond.wait(lock, [&] { return !s.func; });
+}
+
+void Synchronizer::waitForExit() const
+{
+ auto lock = std::unique_lock(mutables);
+ main_blocked = true;
+ while (true) {
+ if (!slots.empty()) {
+ process_slots(lock);
+ } else if (exitposted) {
+ exitposted = false;
+ break;
+ }
+ main_cond.wait(lock);
+ }
+ main_blocked = false;
+}
+
+sigc::connection Synchronizer::connectExit(sigc::slot<void()> const &slot)
+{
+ return signal_exit.connect(slot);
+}
+
+void Synchronizer::awaken() const
+{
+ if (exitposted || !slots.empty()) {
+ return;
+ }
+
+ if (main_blocked) {
+ main_cond.notify_all();
+ } else {
+ const_cast<Glib::Dispatcher&>(dispatcher).emit(); // Glib::Dispatcher is const-incorrect.
+ }
+}
+
+void Synchronizer::on_dispatcher() const
+{
+ auto lock = std::unique_lock(mutables);
+ if (!slots.empty()) {
+ process_slots(lock);
+ } else if (exitposted) {
+ exitposted = false;
+ lock.unlock();
+ signal_exit.emit();
+ }
+}
+
+void Synchronizer::process_slots(std::unique_lock<std::mutex> &lock) const
+{
+ while (!slots.empty()) {
+ auto slots_grabbed = std::move(slots);
+ lock.unlock();
+ for (auto &s : slots_grabbed) {
+ (*s->func)();
+ }
+ lock.lock();
+ for (auto &s : slots_grabbed) {
+ s->func = nullptr;
+ }
+ slots_cond.notify_all();
+ }
+}
+
+} // namespace Inkscape::UI::Widget
+
+/*
+ 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 :
diff --git a/src/ui/widget/canvas/synchronizer.h b/src/ui/widget/canvas/synchronizer.h
new file mode 100644
index 0000000..45c88d2
--- /dev/null
+++ b/src/ui/widget/canvas/synchronizer.h
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_SYNCHRONIZER_H
+#define INKSCAPE_UI_WIDGET_CANVAS_SYNCHRONIZER_H
+
+#include <functional>
+#include <vector>
+#include <mutex>
+#include <condition_variable>
+
+#include <sigc++/sigc++.h>
+#include <glibmm/dispatcher.h>
+
+namespace Inkscape::UI::Widget {
+
+// Synchronisation primitive suiting the canvas's needs. All synchronisation between the main/render threads goes through here.
+class Synchronizer
+{
+public:
+ Synchronizer();
+
+ // Background side:
+
+ // Indicate that the background process has exited, causing EITHER signal_exit to be emitted OR waitforexit() to unblock.
+ void signalExit() const;
+
+ // Block until the given function has executed in the main thread, possibly waking it up if it is itself blocked.
+ // (Note: This is necessary for servicing occasional buffer mapping requests where one can't be pulled from a pool.)
+ void runInMain(std::function<void()> const &f) const;
+
+ // Main-thread side:
+
+ // Block until the background process has exited, gobbling the emission of signal_exit in the process.
+ void waitForExit() const;
+
+ // Connect to signal_exit.
+ sigc::connection connectExit(sigc::slot<void()> const &slot);
+
+private:
+ struct Slot
+ {
+ std::function<void()> const *func;
+ };
+
+ Glib::Dispatcher dispatcher; // Used to wake up main thread if idle in GTK main loop.
+ sigc::signal<void()> signal_exit;
+
+ mutable std::mutex mutables;
+ mutable bool exitposted = false;
+ mutable bool main_blocked = false; // Whether main thread is blocked in waitForExit().
+ mutable std::condition_variable main_cond; // Used to wake up main thread if blocked.
+ mutable std::vector<Slot*> slots; // List of functions from runInMain() waiting to be run.
+ mutable std::condition_variable slots_cond; // Used to wake up render threads blocked in runInMain().
+
+ void awaken() const;
+ void on_dispatcher() const;
+ void process_slots(std::unique_lock<std::mutex> &lock) const;
+};
+
+} // namespace Inkscape::UI::Widget
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_SYNCHRONIZER_H
+
+/*
+ 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 :
diff --git a/src/ui/widget/canvas/texture.cpp b/src/ui/widget/canvas/texture.cpp
new file mode 100644
index 0000000..420937a
--- /dev/null
+++ b/src/ui/widget/canvas/texture.cpp
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "texture.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+static bool have_gltexstorage()
+{
+ static bool result = [] {
+ return epoxy_gl_version() >= 42 || epoxy_has_gl_extension("GL_ARB_texture_storage");
+ }();
+ return result;
+}
+
+static bool have_glinvalidateteximage()
+{
+ static bool result = [] {
+ return epoxy_gl_version() >= 43 || epoxy_has_gl_extension("ARB_invalidate_subdata");
+ }();
+ return result;
+}
+
+Texture::Texture(Geom::IntPoint const &size)
+ : _size(size)
+{
+ glGenTextures(1, &_id);
+ glBindTexture(GL_TEXTURE_2D, _id);
+
+ // Common flags for all textures used at the moment.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+
+ if (have_gltexstorage()) {
+ glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, size.x(), size.y());
+ } else {
+ // Note: This fallback path is always chosen on the Mac due to Apple's crippling of OpenGL.
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, size.x(), size.y(), 0, GL_BGRA, GL_UNSIGNED_BYTE, nullptr);
+ }
+}
+
+void Texture::invalidate()
+{
+ if (have_glinvalidateteximage()) {
+ glInvalidateTexImage(_id, 0);
+ }
+}
+
+} // 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 :
diff --git a/src/ui/widget/canvas/texture.h b/src/ui/widget/canvas/texture.h
new file mode 100644
index 0000000..98aeba2
--- /dev/null
+++ b/src/ui/widget/canvas/texture.h
@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_TEXTURE_H
+#define INKSCAPE_UI_WIDGET_CANVAS_TEXTURE_H
+
+#include <boost/noncopyable.hpp>
+#include <2geom/point.h>
+#include <epoxy/gl.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class Texture
+{
+public:
+ // Create null texture owning no resources.
+ Texture() = default;
+
+ // Allocate a blank texture of a given size. The texture is bound to GL_TEXTURE_2D.
+ Texture(Geom::IntPoint const &size);
+
+ // Wrap an existing texture.
+ Texture(GLuint id, Geom::IntPoint const &size) : _id(id), _size(size) {}
+
+ // Boilerplate constructors/operators
+ Texture(Texture &&other) noexcept { _movefrom(other); }
+ Texture &operator=(Texture &&other) noexcept { _reset(); _movefrom(other); return *this; }
+ ~Texture() { _reset(); }
+
+ // Observers
+ GLuint id() const { return _id; }
+ Geom::IntPoint const &size() const { return _size; }
+ explicit operator bool() const { return _id; }
+
+ // Methods
+ void clear() { _reset(); _id = 0; }
+ void invalidate();
+
+private:
+ GLuint _id = 0;
+ Geom::IntPoint _size;
+
+ void _reset() noexcept { if (_id) glDeleteTextures(1, &_id); }
+ void _movefrom(Texture &other) noexcept { _id = other._id; _size = other._size; other._id = 0; }
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_TEXTURE_H
+
+/*
+ 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 :
diff --git a/src/ui/widget/canvas/texturecache.cpp b/src/ui/widget/canvas/texturecache.cpp
new file mode 100644
index 0000000..6215849
--- /dev/null
+++ b/src/ui/widget/canvas/texturecache.cpp
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include <unordered_map>
+#include <vector>
+#include <cassert>
+#include <boost/unordered_map.hpp> // For hash of pair
+#include "helper/mathfns.h"
+#include "texturecache.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+namespace {
+
+class BasicTextureCache : public TextureCache
+{
+ static int constexpr min_dimension = 16;
+ static int constexpr expiration_timeout = 10000;
+
+ static int constexpr dim_to_ind(int dim) { return Util::floorlog2((dim - 1) / min_dimension) + 1; }
+ static int constexpr ind_to_maxdim(int index) { return min_dimension * (1 << index); }
+
+ static std::pair<int, int> dims_to_inds(Geom::IntPoint const &dims) { return { dim_to_ind(dims.x()), dim_to_ind(dims.y()) }; }
+ static Geom::IntPoint inds_to_maxdims(std::pair<int, int> const &inds) { return { ind_to_maxdim(inds.first), ind_to_maxdim(inds.second) }; }
+
+ // A cache of POT textures.
+ struct Bucket
+ {
+ std::vector<Texture> unused;
+ int used = 0;
+ int high_use_count = 0;
+ };
+ boost::unordered_map<std::pair<int, int>, Bucket> buckets;
+
+ // Used to periodicially discard excess cached textures.
+ int expiration_timer = 0;
+
+public:
+ Texture request(Geom::IntPoint const &dimensions) override
+ {
+ // Find the bucket that the dimensions fall into.
+ auto indexes = dims_to_inds(dimensions);
+ auto &b = buckets[indexes];
+
+ // Reuse or create a texture of the appropriate dimensions.
+ Texture tex;
+ if (!b.unused.empty()) {
+ tex = std::move(b.unused.back());
+ b.unused.pop_back();
+ glBindTexture(GL_TEXTURE_2D, tex.id());
+ } else {
+ tex = Texture(inds_to_maxdims(indexes)); // binds
+ }
+
+ // 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 this, and reset the timer for when to clean up excess unused textures.
+ b.high_use_count = b.used;
+ expiration_timer = 0;
+ }
+
+ return tex;
+ }
+
+ void finish(Texture tex) override
+ {
+ auto indexes = dims_to_inds(tex.size());
+ auto &b = buckets[indexes];
+
+ // Orphan the texture, if possible.
+ tex.invalidate();
+
+ // Put the texture back in its corresponding bucket's cache of unused textures.
+ b.unused.emplace_back(std::move(tex));
+ b.used--;
+
+ // If the expiration timeout has been reached, prune the cache of textures down to what was actually used in the last cycle.
+ expiration_timer++;
+ if (expiration_timer >= expiration_timeout) {
+ expiration_timer = 0;
+
+ for (auto &[k, b] : buckets) {
+ int max_unused = b.high_use_count - b.used;
+ assert(max_unused >= 0);
+ if (b.unused.size() > max_unused) {
+ b.unused.resize(max_unused);
+ }
+ b.high_use_count = b.used;
+ }
+ }
+ }
+};
+
+} // namespace
+
+std::unique_ptr<TextureCache> TextureCache::create()
+{
+ return std::make_unique<BasicTextureCache>();
+}
+
+} // 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 :
diff --git a/src/ui/widget/canvas/texturecache.h b/src/ui/widget/canvas/texturecache.h
new file mode 100644
index 0000000..ea78a67
--- /dev/null
+++ b/src/ui/widget/canvas/texturecache.h
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Extremely basic gadget for re-using textures, since texture creation turns out to be quite expensive.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_TEXTURECACHE_H
+#define INKSCAPE_UI_WIDGET_CANVAS_TEXTURECACHE_H
+
+#include <memory>
+#include "texture.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class TextureCache
+{
+public:
+ virtual ~TextureCache() = default;
+
+ static std::unique_ptr<TextureCache> create();
+
+ /**
+ * Request a texture of at least the given dimensions.
+ * The texture is bound to GL_TEXTURE_2D.
+ */
+ virtual Texture request(Geom::IntPoint const &dimensions) = 0;
+
+ /**
+ * Return a no-longer used texture to the pool.
+ */
+ virtual void finish(Texture tex) = 0;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_TEXTURECACHE_H
+
+/*
+ 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 :
diff --git a/src/ui/widget/canvas/updaters.cpp b/src/ui/widget/canvas/updaters.cpp
new file mode 100644
index 0000000..8441be0
--- /dev/null
+++ b/src/ui/widget/canvas/updaters.cpp
@@ -0,0 +1,235 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "updaters.h"
+#include "ui/util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+class ResponsiveUpdater : public Updater
+{
+public:
+ Strategy get_strategy() const override { return Strategy::Responsive; }
+
+ void reset() override { clean_region = Cairo::Region::create(); }
+ void intersect (Geom::IntRect const &rect) override { clean_region->intersect(geom_to_cairo(rect)); }
+ void mark_dirty(Geom::IntRect const &rect) override { clean_region->subtract(geom_to_cairo(rect)); }
+ void mark_dirty(Cairo::RefPtr<Cairo::Region> const &reg) override { clean_region->subtract(reg); }
+ void mark_clean(Geom::IntRect const &rect) override { clean_region->do_union(geom_to_cairo(rect)); }
+
+ Cairo::RefPtr<Cairo::Region> get_next_clean_region() override { return clean_region; }
+ bool report_finished () override { return false; }
+ void next_frame () override {}
+};
+
+class FullRedrawUpdater : public ResponsiveUpdater
+{
+ // Whether we are currently in the middle of a redraw.
+ bool inprogress = false;
+
+ // Contains a copy of the old clean region if damage events occurred during the current redraw, otherwise null.
+ Cairo::RefPtr<Cairo::Region> old_clean_region;
+
+public:
+ Strategy get_strategy() const override { return Strategy::FullRedraw; }
+
+ void reset() override
+ {
+ ResponsiveUpdater::reset();
+ inprogress = false;
+ old_clean_region.clear();
+ }
+
+ void intersect(const Geom::IntRect &rect) override
+ {
+ ResponsiveUpdater::intersect(rect);
+ if (old_clean_region) old_clean_region->intersect(geom_to_cairo(rect));
+ }
+
+ void mark_dirty(Geom::IntRect const &rect) override
+ {
+ if (inprogress && !old_clean_region) old_clean_region = clean_region->copy();
+ ResponsiveUpdater::mark_dirty(rect);
+ }
+
+ void mark_dirty(const Cairo::RefPtr<Cairo::Region> &reg) override
+ {
+ if (inprogress && !old_clean_region) old_clean_region = clean_region->copy();
+ ResponsiveUpdater::mark_dirty(reg);
+ }
+
+ void mark_clean(const Geom::IntRect &rect) override
+ {
+ ResponsiveUpdater::mark_clean(rect);
+ if (old_clean_region) old_clean_region->do_union(geom_to_cairo(rect));
+ }
+
+ Cairo::RefPtr<Cairo::Region> get_next_clean_region() override
+ {
+ inprogress = true;
+ if (!old_clean_region) {
+ return clean_region;
+ } else {
+ return old_clean_region;
+ }
+ }
+
+ bool report_finished() override
+ {
+ assert(inprogress);
+ if (!old_clean_region) {
+ // Completed redraw without being damaged => finished.
+ inprogress = false;
+ return false;
+ } else {
+ // Completed redraw but damage events arrived => ask for another redraw, using the up-to-date clean region.
+ old_clean_region.clear();
+ return true;
+ }
+ }
+};
+
+class MultiscaleUpdater : public ResponsiveUpdater
+{
+ // Whether we are currently in the middle of a redraw.
+ bool inprogress = false;
+
+ // Whether damage events occurred during the current redraw.
+ bool activated = false;
+
+ int counter; // A steadily incrementing counter from which the current scale is derived.
+ int scale; // The current scale to process updates at.
+ int elapsed; // How much time has been spent at the current scale.
+ std::vector<Cairo::RefPtr<Cairo::Region>> blocked; // The region blocked from being updated at each scale.
+
+public:
+ Strategy get_strategy() const override { return Strategy::Multiscale; }
+
+ void reset() override
+ {
+ ResponsiveUpdater::reset();
+ inprogress = activated = false;
+ }
+
+ void intersect(const Geom::IntRect &rect) override
+ {
+ ResponsiveUpdater::intersect(rect);
+ if (activated) {
+ for (auto &reg : blocked) {
+ reg->intersect(geom_to_cairo(rect));
+ }
+ }
+ }
+
+ void mark_dirty(Geom::IntRect const &rect) override
+ {
+ ResponsiveUpdater::mark_dirty(rect);
+ post_mark_dirty();
+ }
+
+ void mark_dirty(const Cairo::RefPtr<Cairo::Region> &reg) override
+ {
+ ResponsiveUpdater::mark_dirty(reg);
+ post_mark_dirty();
+ }
+
+ void post_mark_dirty()
+ {
+ if (inprogress && !activated) {
+ counter = scale = elapsed = 0;
+ blocked = { Cairo::Region::create() };
+ activated = true;
+ }
+ }
+
+ void mark_clean(const Geom::IntRect &rect) override
+ {
+ ResponsiveUpdater::mark_clean(rect);
+ if (activated) blocked[scale]->do_union(geom_to_cairo(rect));
+ }
+
+ Cairo::RefPtr<Cairo::Region> get_next_clean_region() override
+ {
+ inprogress = true;
+ if (!activated) {
+ return clean_region;
+ } else {
+ auto result = clean_region->copy();
+ result->do_union(blocked[scale]);
+ return result;
+ }
+ }
+
+ bool report_finished() override
+ {
+ assert(inprogress);
+ if (!activated) {
+ // Completed redraw without damage => finished.
+ inprogress = false;
+ return false;
+ } else {
+ // Completed redraw but damage events arrived => begin updating any remaining damaged regions.
+ activated = false;
+ blocked.clear();
+ return true;
+ }
+ }
+
+ void next_frame() override
+ {
+ if (!activated) return;
+
+ // Stay at the current scale for 2^scale frames.
+ elapsed++;
+ if (elapsed < (1 << scale)) return;
+ elapsed = 0;
+
+ // Adjust the counter, which causes scale to hop around the values 0, 1, 2... spending half as much time at each subsequent scale.
+ counter++;
+ scale = 0;
+ for (int tmp = counter; tmp % 2 == 1; tmp /= 2) {
+ scale++;
+ }
+
+ // Ensure sufficiently many blocked zones exist.
+ if (scale == blocked.size()) {
+ blocked.emplace_back();
+ }
+
+ // Recreate the current blocked zone as the union of the clean region and lower-scale blocked zones.
+ blocked[scale] = clean_region->copy();
+ for (int i = 0; i < scale; i++) {
+ blocked[scale]->do_union(blocked[i]);
+ }
+ }
+};
+
+template<> std::unique_ptr<Updater> Updater::create<Updater::Strategy::Responsive>() {return std::make_unique<ResponsiveUpdater>();}
+template<> std::unique_ptr<Updater> Updater::create<Updater::Strategy::FullRedraw>() {return std::make_unique<FullRedrawUpdater>();}
+template<> std::unique_ptr<Updater> Updater::create<Updater::Strategy::Multiscale>() {return std::make_unique<MultiscaleUpdater>();}
+
+std::unique_ptr<Updater> Updater::create(Strategy strategy)
+{
+ switch (strategy)
+ {
+ case Strategy::Responsive: return create<Strategy::Responsive>();
+ case Strategy::FullRedraw: return create<Strategy::FullRedraw>();
+ case Strategy::Multiscale: return create<Strategy::Multiscale>();
+ default: return nullptr; // Never triggered, but GCC errors out on build without.
+ }
+}
+
+} // 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 :
diff --git a/src/ui/widget/canvas/updaters.h b/src/ui/widget/canvas/updaters.h
new file mode 100644
index 0000000..d36685a
--- /dev/null
+++ b/src/ui/widget/canvas/updaters.h
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Controls the order to update invalidated regions.
+ * Copyright (C) 2022 PBS <pbs3141@gmail.com>
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_UPDATERS_H
+#define INKSCAPE_UI_WIDGET_CANVAS_UPDATERS_H
+
+#include <vector>
+#include <memory>
+#include <2geom/int-rect.h>
+#include <cairomm/refptr.h>
+#include <cairomm/region.h>
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+// A class for tracking invalidation events and producing redraw regions.
+class Updater
+{
+public:
+ virtual ~Updater() = default;
+
+ // The subregion of the store with up-to-date content.
+ Cairo::RefPtr<Cairo::Region> clean_region;
+
+ enum class Strategy
+ {
+ Responsive, // As soon as a region is invalidated, redraw it.
+ FullRedraw, // When a region is invalidated, delay redraw until after the current redraw is completed.
+ Multiscale, // Updates tiles near the mouse faster. Gives the best of both.
+ };
+
+ // Create an Updater using the given strategy.
+ template <Strategy strategy>
+ static std::unique_ptr<Updater> create();
+
+ // Create an Updater using a choice of strategy specified at runtime.
+ static std::unique_ptr<Updater> create(Strategy strategy);
+
+ // Return the strategy in use.
+ virtual Strategy get_strategy() const = 0;
+
+ virtual void reset() = 0; // Reset the clean region to empty.
+ virtual void intersect (Geom::IntRect const &) = 0; // Called when the store changes position; clip everything to the new store rectangle.
+ virtual void mark_dirty(Geom::IntRect const &) = 0; // Called on every invalidate event.
+ virtual void mark_dirty(Cairo::RefPtr<Cairo::Region> const &) = 0; // Called on every invalidate event.
+ virtual void mark_clean(Geom::IntRect const &) = 0; // Called on every rectangle redrawn.
+
+ // Called at the start of a redraw to determine what region to consider clean (i.e. will not be drawn).
+ virtual Cairo::RefPtr<Cairo::Region> get_next_clean_region() = 0;
+
+ // Called after a redraw has finished. Returns true to indicate that further redraws are required with different clean regions.
+ virtual bool report_finished() = 0;
+
+ // Called at the start of each frame. Some updaters (Multiscale) require this information.
+ virtual void next_frame() = 0;
+};
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_UPDATERS_H
+
+/*
+ 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 :
diff --git a/src/ui/widget/canvas/util.cpp b/src/ui/widget/canvas/util.cpp
new file mode 100644
index 0000000..3d9d59b
--- /dev/null
+++ b/src/ui/widget/canvas/util.cpp
@@ -0,0 +1,70 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#include "ui/util.h"
+#include "helper/geom.h"
+#include "util.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+void region_to_path(Cairo::RefPtr<Cairo::Context> const &cr, Cairo::RefPtr<Cairo::Region> const &reg)
+{
+ for (int i = 0; i < reg->get_num_rectangles(); i++) {
+ auto rect = reg->get_rectangle(i);
+ cr->rectangle(rect.x, rect.y, rect.width, rect.height);
+ }
+}
+
+Cairo::RefPtr<Cairo::Region> shrink_region(Cairo::RefPtr<Cairo::Region> const &reg, int d, int t)
+{
+ // Find the bounding rect, expanded by 1 in all directions.
+ auto rect = geom_to_cairo(expandedBy(cairo_to_geom(reg->get_extents()), 1));
+
+ // Take the complement of the region within the rect.
+ auto reg2 = Cairo::Region::create(rect);
+ reg2->subtract(reg);
+
+ // Increase the width and height of every rectangle by d.
+ auto reg3 = Cairo::Region::create();
+ for (int i = 0; i < reg2->get_num_rectangles(); i++) {
+ auto rect = reg2->get_rectangle(i);
+ rect.x += t;
+ rect.y += t;
+ rect.width += d;
+ rect.height += d;
+ reg3->do_union(rect);
+ }
+
+ // Take the complement of the region within the rect.
+ reg2 = Cairo::Region::create(rect);
+ reg2->subtract(reg3);
+
+ return reg2;
+}
+
+std::array<float, 3> checkerboard_darken(std::array<float, 3> const &rgb, float amount)
+{
+ std::array<float, 3> hsl;
+ SPColor::rgb_to_hsl_floatv(&hsl[0], rgb[0], rgb[1], rgb[2]);
+ hsl[2] += (hsl[2] < 0.08 ? 0.08 : -0.08) * amount;
+
+ std::array<float, 3> rgb2;
+ SPColor::hsl_to_rgb_floatv(&rgb2[0], hsl[0], hsl[1], hsl[2]);
+
+ return rgb2;
+}
+
+} // 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 :
diff --git a/src/ui/widget/canvas/util.h b/src/ui/widget/canvas/util.h
new file mode 100644
index 0000000..c2c1ad3
--- /dev/null
+++ b/src/ui/widget/canvas/util.h
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+#ifndef INKSCAPE_UI_WIDGET_CANVAS_UTIL_H
+#define INKSCAPE_UI_WIDGET_CANVAS_UTIL_H
+
+#include <array>
+#include <2geom/int-rect.h>
+#include <2geom/affine.h>
+#include <cairomm/cairomm.h>
+#include "color.h"
+
+namespace Inkscape {
+namespace UI {
+namespace Widget {
+
+// Cairo additions
+
+/**
+ * Turn a Cairo region into a path on a given Cairo context.
+ */
+void region_to_path(Cairo::RefPtr<Cairo::Context> const &cr, Cairo::RefPtr<Cairo::Region> const &reg);
+
+/**
+ * Shrink a region by d/2 in all directions, while also translating it by (d/2 + t, d/2 + t).
+ */
+Cairo::RefPtr<Cairo::Region> shrink_region(Cairo::RefPtr<Cairo::Region> const &reg, int d, int t = 0);
+
+inline auto unioned(Cairo::RefPtr<Cairo::Region> a, Cairo::RefPtr<Cairo::Region> const &b)
+{
+ a->do_union(b);
+ return a;
+}
+
+// Colour operations
+
+inline auto rgb_to_array(uint32_t rgb)
+{
+ return std::array{SP_RGBA32_R_U(rgb) / 255.0f, SP_RGBA32_G_U(rgb) / 255.0f, SP_RGBA32_B_U(rgb) / 255.0f};
+}
+
+inline auto rgba_to_array(uint32_t rgba)
+{
+ return std::array{SP_RGBA32_R_U(rgba) / 255.0f, SP_RGBA32_G_U(rgba) / 255.0f, SP_RGBA32_B_U(rgba) / 255.0f, SP_RGBA32_A_U(rgba) / 255.0f};
+}
+
+inline auto premultiplied(std::array<float, 4> arr)
+{
+ arr[0] *= arr[3];
+ arr[1] *= arr[3];
+ arr[2] *= arr[3];
+ return arr;
+}
+
+std::array<float, 3> checkerboard_darken(std::array<float, 3> const &rgb, float amount = 1.0f);
+
+inline auto checkerboard_darken(uint32_t rgba)
+{
+ return checkerboard_darken(rgb_to_array(rgba), 1.0f - SP_RGBA32_A_U(rgba) / 255.0f);
+}
+
+} // namespace Widget
+} // namespace UI
+} // namespace Inkscape
+
+#endif // INKSCAPE_UI_WIDGET_CANVAS_UTIL_H
+
+/*
+ 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 :