From ece2ab0bdddc53154fd7910539b506e5b085d626 Mon Sep 17 00:00:00 2001 From: PBS Date: Thu, 3 Nov 2022 16:53:03 +0900 Subject: [PATCH 1/2] Add support for CanvasItem snapshotting Entirely analogous to efd4e8ca, implement a snapshotting mechanism for CanvasItem to enable safe asynchronous rendering in the next commit. --- src/display/control/canvas-item-bpath.cpp | 23 +++-- src/display/control/canvas-item-context.cpp | 13 +++ src/display/control/canvas-item-context.h | 15 +++ src/display/control/canvas-item-ctrl.cpp | 67 +++++++------ src/display/control/canvas-item-curve.cpp | 32 +++--- src/display/control/canvas-item-grid.cpp | 64 +++++++----- src/display/control/canvas-item-guideline.cpp | 15 +-- src/display/control/canvas-item-quad.cpp | 18 ++-- src/display/control/canvas-item-rect.cpp | 27 +++-- src/display/control/canvas-item-text.cpp | 58 ++++++----- src/display/control/canvas-item.cpp | 98 +++++++++++-------- src/display/control/canvas-item.h | 4 + 12 files changed, 274 insertions(+), 160 deletions(-) diff --git a/src/display/control/canvas-item-bpath.cpp b/src/display/control/canvas-item-bpath.cpp index acad9855d2..339bceef75 100644 --- a/src/display/control/canvas-item-bpath.cpp +++ b/src/display/control/canvas-item-bpath.cpp @@ -59,10 +59,11 @@ void CanvasItemBpath::set_bpath(SPCurve const *curve, bool phantom_line) */ void CanvasItemBpath::set_bpath(Geom::PathVector path, bool phantom_line) { - _path = std::move(path); - _phantom_line = phantom_line; - - request_update(); + defer([=, path = std::move(path)] () mutable { + _path = std::move(path); + _phantom_line = phantom_line; + request_update(); + }); } /** @@ -70,16 +71,19 @@ void CanvasItemBpath::set_bpath(Geom::PathVector path, bool phantom_line) */ void CanvasItemBpath::set_fill(uint32_t fill, SPWindRule fill_rule) { - if (_fill != fill || _fill_rule != fill_rule) { + defer([=] { + if (_fill == fill && _fill_rule == fill_rule) return; _fill = fill; _fill_rule = fill_rule; request_redraw(); - } + }); } void CanvasItemBpath::set_dashes(std::vector &&dashes) { - _dashes = std::move(dashes); + defer([=, dashes = std::move(dashes)] () mutable { + _dashes = std::move(dashes); + }); } /** @@ -87,10 +91,11 @@ void CanvasItemBpath::set_dashes(std::vector &&dashes) */ void CanvasItemBpath::set_stroke_width(double width) { - if (_stroke_width != width) { + defer([=] { + if (_stroke_width == width) return; _stroke_width = width; request_redraw(); - } + }); } /** diff --git a/src/display/control/canvas-item-context.cpp b/src/display/control/canvas-item-context.cpp index 5e1af73c00..99afdc4ca6 100644 --- a/src/display/control/canvas-item-context.cpp +++ b/src/display/control/canvas-item-context.cpp @@ -16,6 +16,19 @@ CanvasItemContext::~CanvasItemContext() delete _root; } +void CanvasItemContext::snapshot() +{ + assert(!_snapshotted); + _snapshotted = true; +} + +void CanvasItemContext::unsnapshot() +{ + assert(_snapshotted); + _snapshotted = false; + _funclog(); +} + } // namespace Inkscape /* diff --git a/src/display/control/canvas-item-context.h b/src/display/control/canvas-item-context.h index da4d57a1be..117110ac4f 100644 --- a/src/display/control/canvas-item-context.h +++ b/src/display/control/canvas-item-context.h @@ -6,6 +6,7 @@ #define SEEN_CANVAS_ITEM_CONTEXT_H #include <2geom/affine.h> +#include "util/funclog.h" namespace Inkscape { @@ -28,6 +29,14 @@ public: Geom::Affine const &affine() const { return _affine; } void setAffine(Geom::Affine const &affine) { _affine = affine; } + // Snapshotting + void snapshot(); + void unsnapshot(); + bool snapshotted() const { return _snapshotted; } + + template + void defer(F &&f) { _snapshotted ? _funclog.emplace(std::forward(f)) : f(); } + private: // Structure UI::Widget::Canvas *_canvas; @@ -35,6 +44,12 @@ private: // Geometry Geom::Affine _affine; + + // Snapshotting + char _cacheline_separator[127]; + + bool _snapshotted = false; + Util::FuncLog _funclog; }; } // namespace Inkscape diff --git a/src/display/control/canvas-item-ctrl.cpp b/src/display/control/canvas-item-ctrl.cpp index a03198f034..d534768372 100644 --- a/src/display/control/canvas-item-ctrl.cpp +++ b/src/display/control/canvas-item-ctrl.cpp @@ -88,10 +88,11 @@ CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlShape shape void CanvasItemCtrl::set_position(Geom::Point const &position) { // std::cout << "CanvasItemCtrl::set_ctrl: " << _name << ": " << position << std::endl; - if (_position != position) { + defer([=] { + if (_position == position) return; _position = position; request_update(); - } + }); } /** @@ -365,29 +366,32 @@ void CanvasItemCtrl::_render(CanvasItemBuffer &buf) void CanvasItemCtrl::set_fill(uint32_t fill) { - if (_fill != fill) { + defer([=] { + if (_fill == fill) return; _fill = fill; _built = false; request_redraw(); - } + }); } void CanvasItemCtrl::set_stroke(uint32_t stroke) { - if (_stroke != stroke) { + defer([=] { + if (_stroke == stroke) return; _stroke = stroke; _built = false; request_redraw(); - } + }); } void CanvasItemCtrl::set_shape(CanvasItemCtrlShape shape) { - if (_shape != shape) { + defer([=] { + if (_shape == shape) return; _shape = shape; _built = false; request_update(); // Geometry could change - } + }); } void CanvasItemCtrl::set_shape_default() @@ -448,37 +452,40 @@ void CanvasItemCtrl::set_shape_default() void CanvasItemCtrl::set_mode(CanvasItemCtrlMode mode) { - if (_mode != mode) { + defer([=] { + if (_mode == mode) return; _mode = mode; _built = false; request_update(); - } + }); } void CanvasItemCtrl::set_pixbuf(Glib::RefPtr pixbuf) { - if (_pixbuf != pixbuf) { + defer([=, pixbuf = std::move(pixbuf)] () mutable { + if (_pixbuf != pixbuf) return; _pixbuf = std::move(pixbuf); _width = _pixbuf->get_width(); _height = _pixbuf->get_height(); _built = false; request_update(); - } + }); } // Nominally width == height == size except possibly for pixmaps. void CanvasItemCtrl::set_size(int size) { - if (_pixbuf) { - // std::cerr << "CanvasItemCtrl::set_size: Attempting to set size on pixbuf control!" << std::endl; - return; - } - if (_width != size + _extra || _height != size + _extra) { + defer([=] { + if (_pixbuf) { + // std::cerr << "CanvasItemCtrl::set_size: Attempting to set size on pixbuf control!" << std::endl; + return; + } + if (_width == size + _extra && _height == size + _extra) return; _width = size + _extra; _height = size + _extra; _built = false; request_update(); // Geometry change - } + }); } void CanvasItemCtrl::set_size_via_index(int size_index) @@ -554,18 +561,20 @@ void CanvasItemCtrl::set_size_default() void CanvasItemCtrl::set_size_extra(int extra) { - if (_extra != extra && !_pixbuf) { // Don't enlarge pixbuf! - _width += (extra - _extra); - _height += (extra - _extra); + defer([=] { + if (_extra == extra || _pixbuf) return; // Don't enlarge pixbuf! + _width += extra - _extra; + _height += extra - _extra; _extra = extra; _built = false; request_update(); // Geometry change - } + }); } void CanvasItemCtrl::set_type(CanvasItemCtrlType type) { - if (_type != type) { + defer([=] { + if (_type == type) return; _type = type; // Use _type to set default values. @@ -573,23 +582,25 @@ void CanvasItemCtrl::set_type(CanvasItemCtrlType type) set_size_default(); _built = false; request_update(); // Possible geometry change - } + }); } void CanvasItemCtrl::set_angle(double angle) { - if (_angle != angle) { + defer([=] { + if (_angle == angle) return; _angle = angle; request_update(); // Geometry change - } + }); } void CanvasItemCtrl::set_anchor(SPAnchorType anchor) { - if (_anchor != anchor) { + defer([=] { + if (_anchor == anchor) return; _anchor = anchor; request_update(); // Geometry change - } + }); } // ---------- Protected ---------- diff --git a/src/display/control/canvas-item-curve.cpp b/src/display/control/canvas-item-curve.cpp index 3a6524d0ae..60fc1fa9c6 100644 --- a/src/display/control/canvas-item-curve.cpp +++ b/src/display/control/canvas-item-curve.cpp @@ -61,10 +61,11 @@ CanvasItemCurve::CanvasItemCurve(CanvasItemGroup *group, */ void CanvasItemCurve::set_coords(Geom::Point const &p0, Geom::Point const &p1) { - _name = "CanvasItemCurve:Line"; - _curve = std::make_unique(p0, p1); - - request_update(); + defer([=] { + _name = "CanvasItemCurve:Line"; + _curve = std::make_unique(p0, p1); + request_update(); + }); } /** @@ -72,10 +73,11 @@ void CanvasItemCurve::set_coords(Geom::Point const &p0, Geom::Point const &p1) */ void CanvasItemCurve::set_coords(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3) { - _name = "CanvasItemCurve:CubicBezier"; - _curve = std::make_unique(p0, p1, p2, p3); - - request_update(); + defer([=] { + _name = "CanvasItemCurve:CubicBezier"; + _curve = std::make_unique(p0, p1, p2, p3); + request_update(); + }); } /** @@ -83,8 +85,11 @@ void CanvasItemCurve::set_coords(Geom::Point const &p0, Geom::Point const &p1, G */ void CanvasItemCurve::set_width(int width) { - _width = width; - request_update(); + defer([=] { + if (_width == width) return; + _width = width; + request_update(); + }); } /** @@ -92,8 +97,11 @@ void CanvasItemCurve::set_width(int width) */ void CanvasItemCurve::set_bg_alpha(float alpha) { - bg_alpha = alpha; - request_update(); + defer([=] { + if (bg_alpha == alpha) return; + bg_alpha = alpha; + request_update(); + }); } /** diff --git a/src/display/control/canvas-item-grid.cpp b/src/display/control/canvas-item-grid.cpp index f3ad7233ef..fee913aa8b 100644 --- a/src/display/control/canvas-item-grid.cpp +++ b/src/display/control/canvas-item-grid.cpp @@ -93,39 +93,57 @@ static std::vector intersect_line_rectangle(Geom::Line const &line, void CanvasItemGrid::set_origin(Geom::Point const &point) { - _origin = point; - request_update(); + defer([=] { + if (_origin == point) return; + _origin = point; + request_update(); + }); } void CanvasItemGrid::set_major_color(uint32_t color) { - _major_color = color; - request_update(); + defer([=] { + if (_major_color == color) return; + _major_color = color; + request_update(); + }); } void CanvasItemGrid::set_minor_color(uint32_t color) { - _minor_color = color; - request_update(); + defer([=] { + if (_minor_color == color) return; + _minor_color = color; + request_update(); + }); } void CanvasItemGrid::set_dotted(bool dotted) { - _dotted = dotted; - request_update(); + defer([=] { + if (_dotted == dotted) return; + _dotted = dotted; + request_update(); + }); } void CanvasItemGrid::set_spacing(Geom::Point const &point) { - _spacing = point; - request_update(); + defer([=] { + if (_spacing == point) return; + _spacing = point; + request_update(); + }); } void CanvasItemGrid::set_major_line_interval(int n) { if (n < 1) return; - _major_line_interval = n; - request_update(); + defer([=] { + if (_major_line_interval == n) return; + _major_line_interval = n; + request_update(); + }); } void CanvasItemGrid::set_no_emp_when_zoomed_out(bool noemp) @@ -338,21 +356,23 @@ void CanvasItemGridAxonom::_update(bool) // expects value given to be in degrees void CanvasItemGridAxonom::set_angle_x(double deg) { - angle_deg[X] = std::clamp(deg, 0.0, 89.0); // setting to 90 and values close cause extreme slowdowns - angle_rad[X] = Geom::rad_from_deg(angle_deg[X]); - tan_angle[X] = std::tan(angle_rad[X]); - - request_update(); + defer([=] { + angle_deg[X] = std::clamp(deg, 0.0, 89.0); // setting to 90 and values close cause extreme slowdowns + angle_rad[X] = Geom::rad_from_deg(angle_deg[X]); + tan_angle[X] = std::tan(angle_rad[X]); + request_update(); + }); } // expects value given to be in degrees void CanvasItemGridAxonom::set_angle_z(double deg) { - angle_deg[Z] = std::clamp(deg, 0.0, 89.0); // setting to 90 and values close cause extreme slowdowns - angle_rad[Z] = Geom::rad_from_deg(angle_deg[Z]); - tan_angle[Z] = std::tan(angle_rad[Z]); - - request_update(); + defer([=] { + angle_deg[Z] = std::clamp(deg, 0.0, 89.0); // setting to 90 and values close cause extreme slowdowns + angle_rad[Z] = Geom::rad_from_deg(angle_deg[Z]); + tan_angle[Z] = std::tan(angle_rad[Z]); + request_update(); + }); } static void drawline(Inkscape::CanvasItemBuffer &buf, int x0, int y0, int x1, int y1, uint32_t rgba) diff --git a/src/display/control/canvas-item-guideline.cpp b/src/display/control/canvas-item-guideline.cpp index 966ae69cf0..d1f5a8b3e0 100644 --- a/src/display/control/canvas-item-guideline.cpp +++ b/src/display/control/canvas-item-guideline.cpp @@ -229,15 +229,17 @@ void CanvasItemGuideLine::set_stroke(uint32_t color) void CanvasItemGuideLine::set_label(Glib::ustring &&label) { - if (_label != label) { + defer([=, label = std::move(label)] () mutable { + if (_label == label) return; _label = std::move(label); request_update(); - } + }); } void CanvasItemGuideLine::set_locked(bool locked) { - if (_locked != locked) { + defer([=] { + if (_locked == locked) return; _locked = locked; if (_locked) { _origin_ctrl->set_shape(CANVAS_ITEM_CTRL_SHAPE_CROSS); @@ -248,7 +250,7 @@ void CanvasItemGuideLine::set_locked(bool locked) _origin_ctrl->set_stroke(0x00000000); // no stroke _origin_ctrl->set_fill(_stroke); // fill the control with this guide's color } - } + }); } //=============================================================================================== @@ -286,13 +288,14 @@ void CanvasItemGuideHandle::set_size_via_index(int index) if (size < MINIMUM_SIZE) { size = MINIMUM_SIZE; } - if (_width != size) { + defer([=] { + if (_width == size) return; _width = size; _height = size; _built = false; request_update(); _my_line->request_update(); - } + }); } } // namespace Inkscape diff --git a/src/display/control/canvas-item-quad.cpp b/src/display/control/canvas-item-quad.cpp index f02fffc769..93b2bc4b61 100644 --- a/src/display/control/canvas-item-quad.cpp +++ b/src/display/control/canvas-item-quad.cpp @@ -52,12 +52,13 @@ CanvasItemQuad::CanvasItemQuad(CanvasItemGroup *group, */ void CanvasItemQuad::set_coords(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3) { - _p0 = p0; - _p1 = p1; - _p2 = p2; - _p3 = p3; - - request_update(); + defer([=] { + _p0 = p0; + _p1 = p1; + _p2 = p2; + _p3 = p3; + request_update(); + }); } /** @@ -148,10 +149,11 @@ void CanvasItemQuad::_render(Inkscape::CanvasItemBuffer &buf) void CanvasItemQuad::set_inverted(bool inverted) { - if (_inverted != inverted) { + defer([=] { + if (_inverted == inverted) return; _inverted = inverted; request_redraw(); - } + }); } } // namespace Inkscape diff --git a/src/display/control/canvas-item-rect.cpp b/src/display/control/canvas-item-rect.cpp index d821087621..d8c616ee1c 100644 --- a/src/display/control/canvas-item-rect.cpp +++ b/src/display/control/canvas-item-rect.cpp @@ -51,8 +51,11 @@ CanvasItemRect::CanvasItemRect(CanvasItemGroup *group, Geom::Rect const &rect) */ void CanvasItemRect::set_rect(Geom::Rect const &rect) { - _rect = rect; - request_update(); + defer([=] { + if (_rect == rect) return; + _rect = rect; + request_update(); + }); } /** @@ -187,10 +190,11 @@ void CanvasItemRect::_render(Inkscape::CanvasItemBuffer &buf) void CanvasItemRect::set_is_page(bool is_page) { - if (_is_page != is_page) { + defer([=] { + if (_is_page == is_page) return; _is_page = is_page; request_redraw(); - } + }); } void CanvasItemRect::set_fill(uint32_t fill) @@ -201,28 +205,31 @@ void CanvasItemRect::set_fill(uint32_t fill) void CanvasItemRect::set_dashed(bool dashed) { - if (_dashed != dashed) { + defer([=] { + if (_dashed == dashed) return; _dashed = dashed; request_redraw(); - } + }); } void CanvasItemRect::set_inverted(bool inverted) { - if (_inverted != inverted) { + defer([=] { + if (_inverted == inverted) return; _inverted = inverted; request_redraw(); - } + }); } void CanvasItemRect::set_shadow(uint32_t color, int width) { - if (_shadow_color != color || _shadow_width != width) { + defer([=] { + if (_shadow_color == color && _shadow_width == width) return; _shadow_color = color; _shadow_width = width; request_redraw(); if (_is_page) get_canvas()->set_border(_shadow_width > 0 ? color : 0x0); - } + }); } double CanvasItemRect::get_shadow_size() const diff --git a/src/display/control/canvas-item-text.cpp b/src/display/control/canvas-item-text.cpp index c25740e8a2..63719df737 100644 --- a/src/display/control/canvas-item-text.cpp +++ b/src/display/control/canvas-item-text.cpp @@ -56,9 +56,11 @@ CanvasItemText::CanvasItemText(CanvasItemGroup *group, Geom::Point const &p, Gli */ void CanvasItemText::set_coord(Geom::Point const &p) { - _p = p; - - request_update(); + defer([=] { + if (_p == p) return; + _p = p; + request_update(); + }); } /** @@ -66,9 +68,11 @@ void CanvasItemText::set_coord(Geom::Point const &p) */ void CanvasItemText::set_bg_radius(double rad) { - _bg_rad = rad; - - request_update(); + defer([=] { + if (_bg_rad == rad) return; + _bg_rad = rad; + request_update(); + }); } /** @@ -168,18 +172,20 @@ void CanvasItemText::_render(Inkscape::CanvasItemBuffer &buf) void CanvasItemText::set_text(Glib::ustring text) { - if (_text != text) { + defer([=, text = std::move(text)] () mutable { + if (_text == text) return; _text = std::move(text); request_update(); // Might be larger than before! - } + }); } void CanvasItemText::set_fontsize(double fontsize) { - if (_fontsize != fontsize) { + defer([=] { + if (_fontsize == fontsize) return; _fontsize = fontsize; request_update(); // Might be larger than before! - } + }); } /** @@ -207,11 +213,13 @@ Geom::Rect CanvasItemText::load_text_extents() void CanvasItemText::set_background(uint32_t background) { - if (_background != background) { - _background = background; - request_redraw(); - } - _use_background = true; + defer([=] { + if (_background != background) { + _background = background; + request_redraw(); + } + _use_background = true; + }); } /** @@ -219,34 +227,38 @@ void CanvasItemText::set_background(uint32_t background) */ void CanvasItemText::set_anchor(Geom::Point const &anchor_pt) { - if (_anchor_position != anchor_pt) { + defer([=] { + if (_anchor_position == anchor_pt) return; _anchor_position = anchor_pt; request_update(); - } + }); } void CanvasItemText::set_adjust(Geom::Point const &adjust_pt) { - if (_adjust_offset != adjust_pt) { + defer([=] { + if (_adjust_offset == adjust_pt) return; _adjust_offset = adjust_pt; request_update(); - } + }); } void CanvasItemText::set_fixed_line(bool fixed_line) { - if (_fixed_line != fixed_line) { + defer([=] { + if (_fixed_line == fixed_line) return; _fixed_line = fixed_line; request_update(); - } + }); } void CanvasItemText::set_border(double border) { - if (_border != border) { + defer([=] { + if (_border == border) return; _border = border; request_update(); - } + }); } } // namespace Inkscape diff --git a/src/display/control/canvas-item.cpp b/src/display/control/canvas-item.cpp index 61944142d7..70136a9fba 100644 --- a/src/display/control/canvas-item.cpp +++ b/src/display/control/canvas-item.cpp @@ -38,27 +38,31 @@ CanvasItem::CanvasItem(CanvasItemGroup *parent) , _parent(parent) { if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem: add " << get_name() << " to " << parent->get_name() << " " << parent->items.size() << std::endl; - parent->items.push_back(*this); - request_update(); + defer([=] { + parent->items.push_back(*this); + request_update(); + }); } void CanvasItem::unlink() { - // Clear canvas of item. - request_redraw(); + defer([=] { + // Clear canvas of item. + request_redraw(); - // Remove from parent. - if (_parent) { - if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem: remove " << get_name() << " from " << _parent->get_name() << " " << _parent->items.size() << std::endl; - auto it = _parent->items.iterator_to(*this); - assert(it != _parent->items.end()); - _parent->items.erase(it); - _parent->request_update(); - } else { - if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem: destroy root " << get_name() << std::endl; - } + // Remove from parent. + if (_parent) { + if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem: remove " << get_name() << " from " << _parent->get_name() << " " << _parent->items.size() << std::endl; + auto it = _parent->items.iterator_to(*this); + assert(it != _parent->items.end()); + _parent->items.erase(it); + _parent->request_update(); + } else { + if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem: destroy root " << get_name() << std::endl; + } - delete this; + delete this; + }); } CanvasItem::~CanvasItem() @@ -86,17 +90,19 @@ void CanvasItem::set_z_position(int zpos) return; } - _parent->items.erase(_parent->items.iterator_to(*this)); - - if (zpos <= 0) { - _parent->items.push_front(*this); - } else if (zpos >= _parent->items.size() - 1) { - _parent->items.push_back(*this); - } else { - auto it = _parent->items.begin(); - std::advance(it, zpos); - _parent->items.insert(it, *this); - } + defer([=] { + _parent->items.erase(_parent->items.iterator_to(*this)); + + if (zpos <= 0) { + _parent->items.push_front(*this); + } else if (zpos >= _parent->items.size() - 1) { + _parent->items.push_back(*this); + } else { + auto it = _parent->items.begin(); + std::advance(it, zpos); + _parent->items.insert(it, *this); + } + }); } void CanvasItem::raise_to_top() @@ -106,8 +112,10 @@ void CanvasItem::raise_to_top() return; } - _parent->items.erase(_parent->items.iterator_to(*this)); - _parent->items.push_back(*this); + defer([=] { + _parent->items.erase(_parent->items.iterator_to(*this)); + _parent->items.push_back(*this); + }); } void CanvasItem::lower_to_bottom() @@ -117,8 +125,10 @@ void CanvasItem::lower_to_bottom() return; } - _parent->items.erase(_parent->items.iterator_to(*this)); - _parent->items.push_front(*this); + defer([=] { + _parent->items.erase(_parent->items.iterator_to(*this)); + _parent->items.push_front(*this); + }); } // Indicate geometry changed and bounds needs recalculating. @@ -222,14 +232,16 @@ void CanvasItem::render(CanvasItemBuffer &buf) void CanvasItem::set_visible(bool visible) { - if (_visible == visible) return; - if (_visible) { - request_update(); - _visible = false; - } else { - _visible = true; - request_update(); - } + defer([=] { + if (_visible == visible) return; + if (_visible) { + request_update(); + _visible = false; + } else { + _visible = true; + request_update(); + } + }); } void CanvasItem::request_redraw() @@ -242,18 +254,20 @@ void CanvasItem::request_redraw() void CanvasItem::set_fill(uint32_t fill) { - if (_fill != fill) { + defer([=] { + if (_fill == fill) return; _fill = fill; request_redraw(); - } + }); } void CanvasItem::set_stroke(uint32_t stroke) { - if (_stroke != stroke) { + defer([=] { + if (_stroke == stroke) return; _stroke = stroke; request_redraw(); - } + }); } void CanvasItem::update_canvas_item_ctrl_sizes(int size_index) diff --git a/src/display/control/canvas-item.h b/src/display/control/canvas-item.h index fe5f5f9d33..f4493e0830 100644 --- a/src/display/control/canvas-item.h +++ b/src/display/control/canvas-item.h @@ -141,6 +141,10 @@ protected: // Events sigc::signal _event_signal; + + // Snapshotting + template + void defer(F &&f) { _context->defer(std::forward(f)); } }; } // namespace Inkscape -- GitLab From d40a297a6eac9821552975774e05e82445c48eb9 Mon Sep 17 00:00:00 2001 From: PBS Date: Sat, 11 Jun 2022 21:45:35 +0900 Subject: [PATCH 2/2] Asynchronous Canvas Make the canvas do all its rendering on a single background thread, rather than on the GTK idle loop. This significantly improves responsiveness when panning and zooming, and seems to provide an effective workaround for the GTK-caused extreme slowness on Windows. Other notes: * Event bucketisation is removed, and original event gobblers reinstated. * Rendering preferences are given a tidy-up. * wait_for_drawing_inactive() must be called before averageColor() to avoid concurrent calls to render() while rendering is not yet reentrant. This is only a short-lived temporary measure. * Multithreading support is already present, but will be activated later. --- src/gradient-drag.cpp | 2 +- src/ui/CMakeLists.txt | 2 + src/ui/dialog/inkscape-preferences.cpp | 74 +- src/ui/dialog/inkscape-preferences.h | 18 +- src/ui/knot/knot.cpp | 2 +- src/ui/tool/control-point-selection.cpp | 2 +- src/ui/tools/calligraphic-tool.cpp | 5 +- src/ui/tools/dropper-tool.cpp | 4 +- src/ui/tools/spray-tool.cpp | 6 +- src/ui/tools/tool-base.cpp | 68 +- src/ui/tools/tool-base.h | 6 +- src/ui/widget/canvas.cpp | 1515 ++++++++++++----------- src/ui/widget/canvas.h | 12 +- src/ui/widget/canvas/cairographics.cpp | 82 +- src/ui/widget/canvas/cairographics.h | 2 +- src/ui/widget/canvas/fragment.h | 8 +- src/ui/widget/canvas/framecheck.cpp | 31 +- src/ui/widget/canvas/framecheck.h | 18 +- src/ui/widget/canvas/glgraphics.cpp | 12 +- src/ui/widget/canvas/glgraphics.h | 4 +- src/ui/widget/canvas/graphics.h | 3 +- src/ui/widget/canvas/pixelstreamer.cpp | 224 +++- src/ui/widget/canvas/pixelstreamer.h | 7 +- src/ui/widget/canvas/prefs.h | 60 +- src/ui/widget/canvas/synchronizer.cpp | 103 ++ src/ui/widget/canvas/synchronizer.h | 72 ++ src/ui/widget/canvas/updaters.cpp | 28 +- src/ui/widget/canvas/updaters.h | 9 +- src/ui/widget/preferences-widget.cpp | 2 +- src/ui/widget/preferences-widget.h | 2 +- src/ui/widget/rotateable.cpp | 1 + 31 files changed, 1357 insertions(+), 1027 deletions(-) create mode 100644 src/ui/widget/canvas/synchronizer.cpp create mode 100644 src/ui/widget/canvas/synchronizer.h diff --git a/src/gradient-drag.cpp b/src/gradient-drag.cpp index 090d5c691b..9de3f242c9 100644 --- a/src/gradient-drag.cpp +++ b/src/gradient-drag.cpp @@ -2805,7 +2805,7 @@ bool GrDrag::key_press_handler(GdkEvent *event) y_dir *= -desktop->yaxisdir(); - gint mul = 1 + desktop->canvas->gobble_key_events(keyval, 0); // with any mask + gint mul = 1 + Inkscape::UI::Tools::gobble_key_events(keyval, 0); // with any mask if (MOD__SHIFT(event)) { mul *= 10; diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 9d1673d56e..fe6aada92d 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -169,6 +169,7 @@ set(ui_SRC widget/completion-popup.cpp widget/canvas.cpp widget/canvas/stores.cpp + widget/canvas/synchronizer.cpp widget/canvas/util.cpp widget/canvas/texture.cpp widget/canvas/texturecache.cpp @@ -441,6 +442,7 @@ set(ui_SRC widget/canvas/fragment.h widget/canvas/prefs.h widget/canvas/stores.h + widget/canvas/synchronizer.h widget/canvas/util.h widget/canvas/texture.h widget/canvas/texturecache.h diff --git a/src/ui/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp index 97d17a8ad4..36d38961c9 100644 --- a/src/ui/dialog/inkscape-preferences.cpp +++ b/src/ui/dialog/inkscape-preferences.cpp @@ -2715,16 +2715,12 @@ void InkscapePreferences::initPageRendering() { /* threaded blur */ //related comments/widgets/functions should be renamed and option should be moved elsewhere when inkscape is fully multi-threaded _filter_multi_threaded.init("/options/threading/numthreads", 1.0, 32.0, 1.0, 2.0, 4.0, true, false); - _page_rendering.add_line( false, _("Number of _Threads:"), _filter_multi_threaded, "", _("Configure number of processors/threads to use when rendering filters"), false, reset_icon()); + _page_rendering.add_line( false, _("Number of _Threads:"), _filter_multi_threaded, "", _("Configure number of processors/threads to use when rendering filters"), false); // rendering cache _rendering_cache_size.init("/options/renderingcache/size", 0.0, 4096.0, 1.0, 32.0, 64.0, true, false); _page_rendering.add_line( false, _("Rendering _cache size:"), _rendering_cache_size, C_("mebibyte (2^20 bytes) abbreviation","MiB"), _("Set the amount of memory per document which can be used to store rendered parts of the drawing for later reuse; set to zero to disable caching"), false); - // rendering tile multiplier - _rendering_tile_multiplier.init("/options/rendering/tile-multiplier", 1.0, 512.0, 1.0, 16.0, 16.0, true, false); - _page_rendering.add_line( false, _("Rendering tile multiplier:"), _rendering_tile_multiplier, "", _("On modern hardware, increasing this value (default is 16) can help to get a better performance when there are large areas with filtered objects (this includes blur and blend modes) in your drawing. Decrease the value to make zooming and panning in relevant areas faster on low-end hardware in drawings with few or no filters."), false); - // rendering x-ray radius _rendering_xray_radius.init("/options/rendering/xray-radius", 1.0, 1500.0, 1.0, 100.0, 100.0, true, false); _page_rendering.add_line( false, _("X-ray radius:"), _rendering_xray_radius, "", _("Radius of the circular area around the mouse cursor in X-ray mode"), false); @@ -2735,21 +2731,17 @@ void InkscapePreferences::initPageRendering() // update strategy { - int values[] = {1, 2, 3}; - Glib::ustring labels[] = {_("Responsive"), _("Full redraw"), _("Multiscale")}; + constexpr int values[] = { 1, 2, 3 }; + Glib::ustring const labels[] = { _("Responsive"), _("Full redraw"), _("Multiscale") }; _canvas_update_strategy.init("/options/rendering/update_strategy", labels, values, 3, 3); _page_rendering.add_line(false, _("Update strategy:"), _canvas_update_strategy, "", _("How to update continually changing content when it can't be redrawn fast enough"), false); } - // block updates - _canvas_block_updates.init("", "/options/rendering/block_updates", true); - _page_rendering.add_line(false, _("Use block updates:"), _canvas_block_updates, "", _("Update the dragged region as a single block"), false); - // opengl _canvas_request_opengl.init("", "/options/rendering/request_opengl", false); _page_rendering.add_line( false, _("Enable OpenGL:"), _canvas_request_opengl, "", _("Request that the canvas should be painted with OpenGL rather than Cairo. If OpenGL is unsupported, it will fall back to Cairo."), false); - /* blur quality */ + // blur quality _blur_quality_best.init ( _("Best quality (slowest)"), "/options/blurquality/value", BLUR_QUALITY_BEST, false, nullptr); _blur_quality_better.init ( _("Better quality (slower)"), "/options/blurquality/value", @@ -2773,7 +2765,7 @@ void InkscapePreferences::initPageRendering() _page_rendering.add_line( true, "", _blur_quality_worst, "", _("Lowest quality (considerable artifacts), but display is fastest")); - /* filter quality */ + // filter quality _filter_quality_best.init ( _("Best quality (slowest)"), "/options/filterquality/value", Inkscape::Filters::FILTER_QUALITY_BEST, false, nullptr); _filter_quality_better.init ( _("Better quality (slower)"), "/options/filterquality/value", @@ -2811,15 +2803,15 @@ void InkscapePreferences::initPageRendering() revealer->add(*grid); revealer->set_reveal_child(Inkscape::Preferences::get()->getBool("/options/rendering/devmode")); _canvas_developer_mode_enabled.init(_("Enable developer mode"), "/options/rendering/devmode", false); - _canvas_developer_mode_enabled.signal_toggled().connect([revealer, this] {revealer->set_reveal_child(_canvas_developer_mode_enabled.get_active());}); + _canvas_developer_mode_enabled.signal_toggled().connect([revealer, this] { revealer->set_reveal_child(_canvas_developer_mode_enabled.get_active()); }); _page_rendering.add_group_header(_("Developer mode")); _page_rendering.add_line(true, "", _canvas_developer_mode_enabled, "", _("Enable additional debugging options"), false); _page_rendering.add(*revealer); - auto add_devmode_line = [&] (Glib::ustring const &label, Gtk::Widget &widget, Glib::ustring const &suffix, const Glib::ustring &tip) { + auto add_devmode_line = [&] (Glib::ustring const &label, Gtk::Widget &widget, Glib::ustring const &suffix, Glib::ustring const &tip) { widget.set_tooltip_text(tip); - auto hb = Gtk::manage(new Gtk::Box()); + auto hb = Gtk::make_managed(); hb->set_spacing(12); hb->set_hexpand(true); hb->pack_start(widget, false, false); @@ -2834,14 +2826,14 @@ void InkscapePreferences::initPageRendering() grid->add(*label_widget); grid->attach_next_to(*hb, *label_widget, Gtk::POS_RIGHT, 1, 1); - if (suffix != "") { + if (!suffix.empty()) { auto suffix_widget = Gtk::make_managed(suffix, Gtk::ALIGN_START, Gtk::ALIGN_CENTER, true); suffix_widget->set_markup(suffix_widget->get_text()); hb->pack_start(*suffix_widget, false, false); } }; - auto add_devmode_group_header = [&] (Glib::ustring name) { + auto add_devmode_group_header = [&] (Glib::ustring const &name) { auto label_widget = Gtk::make_managed(Glib::ustring(/*"*/"") + name + Glib::ustring(""/*"*/) , Gtk::ALIGN_START , Gtk::ALIGN_CENTER, true); label_widget->set_use_markup(true); label_widget->set_valign(Gtk::ALIGN_CENTER); @@ -2851,18 +2843,22 @@ void InkscapePreferences::initPageRendering() //TRANSLATORS: The following are options for fine-tuning rendering, meant to be used by developers, //find more explanations at https://gitlab.com/inkscape/inbox/-/issues/6544#note_886540227 add_devmode_group_header(_("Low-level tuning options")); - _canvas_render_time_limit.init("/options/rendering/render_time_limit", 100.0, 1000000.0, 1.0, 0.0, 1000.0, true, false); - add_devmode_line(_("Render time limit"), _canvas_render_time_limit, C_("microsecond abbreviation", "μs"), _("The maximum time allowed for a rendering time slice")); - _canvas_use_new_bisector.init("", "/options/rendering/use_new_bisector", true); - add_devmode_line(_("Use new bisector algorithm"), _canvas_use_new_bisector, "", _("Use an alternative, more obvious bisection strategy: just chop tile in half along the larger dimension until small enough")); - _canvas_new_bisector_size.init("/options/rendering/new_bisector_size", 1.0, 10000.0, 1.0, 0.0, 500.0, true, false); - add_devmode_line(_("Smallest tile size for new bisector"), _canvas_new_bisector_size, C_("pixel abbreviation", "px"), _("Halve rendering tile rectangles until their largest dimension is this small")); - _rendering_tile_size.init("/options/rendering/tile-size", 1.0, 10000.0, 1.0, 0.0, 16.0, true, false); - add_devmode_line(_("Tile size:"), _rendering_tile_size, "", _("The \"tile size\" parameter previously hard-coded into Inkscape's original tile bisector.")); - _canvas_pad.init("/options/rendering/pad", 0.0, 1000.0, 1.0, 0.0, 350.0, true, false); - add_devmode_line(_("Buffer padding"), _canvas_pad, C_("pixel abbreviation", "px"), _("Use buffers bigger than the window by this amount")); - _canvas_margin.init("/options/rendering/margin", 0.0, 1000.0, 1.0, 0.0, 100.0, true, false); - add_devmode_line(_("Prerender margin"), _canvas_margin, "", _("Pre-render a margin around the visible region.")); + _canvas_tile_size.init("/options/rendering/tile_size", 1.0, 10000.0, 1.0, 0.0, 500.0, true, false); + add_devmode_line(_("Tile size"), _canvas_tile_size, "", _("Halve rendering tile rectangles until their largest dimension is this small")); + _canvas_render_time_limit.init("/options/rendering/render_time_limit", 1.0, 5000.0, 1.0, 0.0, 80.0, true, false); + add_devmode_line(_("Render time limit"), _canvas_render_time_limit, C_("millisecond abbreviation", "ms"), _("The maximum time allowed for a rendering time slice")); + _canvas_block_updates.init("", "/options/rendering/block_updates", true); + add_devmode_line(_("Use block updates"), _canvas_block_updates, "", _("Update the dragged region as a single block")); + { + constexpr int values[] = { 1, 2, 3, 4 }; + Glib::ustring const labels[] = { _("Auto"), _("Persistent"), _("Asynchronous"), _("Synchronous") }; + _canvas_pixelstreamer_method.init("/options/rendering/pixelstreamer_method", labels, values, 4, 1); + add_devmode_line(_("Pixel streaming method"), _canvas_pixelstreamer_method, "", _("Change the method used for streaming pixel data to the GPU. The default is Auto, which picks the best method available at runtime. As for the other options, higher up is better. Be warned! No attempt is made to stop you from selecting a method that isn't supported! (This is dev mode, afer all.) If you do so, it will be an instant crash.")); + } + _canvas_padding.init("/options/rendering/padding", 0.0, 1000.0, 1.0, 0.0, 350.0, true, false); + add_devmode_line(_("Buffer padding"), _canvas_padding, C_("pixel abbreviation", "px"), _("Use buffers bigger than the window by this amount")); + _canvas_prerender.init("/options/rendering/prerender", 0.0, 1000.0, 1.0, 0.0, 100.0, true, false); + add_devmode_line(_("Prerender margin"), _canvas_prerender, "", _("Pre-render a margin around the visible region.")); _canvas_preempt.init("/options/rendering/preempt", 0.0, 1000.0, 1.0, 0.0, 250.0, true, false); add_devmode_line(_("Preempt size"), _canvas_preempt, "", _("Prevent thin tiles at the rendering edge by making them at least this size.")); _canvas_coarsener_min_size.init("/options/rendering/coarsener_min_size", 0.0, 1000.0, 1.0, 0.0, 200.0, true, false); @@ -2871,22 +2867,16 @@ void InkscapePreferences::initPageRendering() add_devmode_line(_("Glue size for coarsener algorithm"), _canvas_coarsener_glue_size, C_("pixel abbreviation", "px"), _("Coarsener algorithm absorbs nearby rectangles within this distance.")); _canvas_coarsener_min_fullness.init("/options/rendering/coarsener_min_fullness", 0.0, 1.0, 0.0, 0.0, 0.3, false, false); add_devmode_line(_("Min fullness for coarsener algorithm"), _canvas_coarsener_min_fullness, "", _("Refuse coarsening algorithm's attempt if the result would be more empty than this.")); - { - int values[] = {1, 2, 3, 4}; - Glib::ustring labels[] = {_("Auto"), _("Persistent"), _("Asynchronous"), _("Synchronous")}; - _canvas_pixelstreamer_method.init("/options/rendering/pixelstreamer_method", labels, values, 4, 1); - add_devmode_line(_("Pixel streaming method"), _canvas_pixelstreamer_method, "", _("Change the method used for streaming pixel data to the GPU. The default is Auto, which picks the best method available at runtime. As for the other options, higher up is better. Be warned! No attempt is made to stop you from selecting a method that isn't supported! (This is dev mode, afer all.) If you do so, it will be an instant crash.")); - } add_devmode_group_header(_("Debugging, profiling and experiments")); _canvas_debug_framecheck.init("", "/options/rendering/debug_framecheck", false); add_devmode_line(_("Framecheck"), _canvas_debug_framecheck, "", _("Print profiling data of selected operations to a file")); _canvas_debug_logging.init("", "/options/rendering/debug_logging", false); add_devmode_line(_("Logging"), _canvas_debug_logging, "", _("Log certain events to the console")); - _canvas_debug_slow_redraw.init("", "/options/rendering/debug_slow_redraw", false); - add_devmode_line(_("Slow redraw"), _canvas_debug_slow_redraw, "", _("Introduce a fixed delay for each tile")); - _canvas_debug_slow_redraw_time.init("/options/rendering/debug_slow_redraw_time", 0.0, 1000000.0, 1.0, 0.0, 50.0, true, false); - add_devmode_line(_("Slow redraw time"), _canvas_debug_slow_redraw_time, C_("microsecond abbreviation", "μs"), _("The delay to introduce for each tile")); + _canvas_debug_delay_redraw.init("", "/options/rendering/debug_delay_redraw", false); + add_devmode_line(_("Delay redraw"), _canvas_debug_delay_redraw, "", _("Introduce a fixed delay for each tile")); + _canvas_debug_delay_redraw_time.init("/options/rendering/debug_delay_redraw_time", 0.0, 1000000.0, 1.0, 0.0, 50.0, true, false); + add_devmode_line(_("Delay redraw time"), _canvas_debug_delay_redraw_time, C_("microsecond abbreviation", "μs"), _("The delay to introduce for each tile")); _canvas_debug_show_redraw.init("", "/options/rendering/debug_show_redraw", false); add_devmode_line(_("Show redraw"), _canvas_debug_show_redraw, "", _("Paint a translucent random colour over each newly drawn tile")); _canvas_debug_show_unclean.init("", "/options/rendering/debug_show_unclean", false); @@ -2901,10 +2891,8 @@ void InkscapePreferences::initPageRendering() add_devmode_line(_("Sticky decoupled mode"), _canvas_debug_sticky_decoupled, "", _("Stay in decoupled mode even after rendering is complete")); _canvas_debug_animate.init("", "/options/rendering/debug_animate", false); add_devmode_line(_("Animate"), _canvas_debug_animate, "", _("Continuously adjust viewing parameters in an animation loop.")); - _canvas_debug_idle_starvation.init("", "/options/rendering/debug_idle_starvation", false); - add_devmode_line(_("Print render time stats"), _canvas_debug_idle_starvation, "", _("On display of each frame, log to the console how much time was taken away from rendering, and whether rendering is still busy. A high value would explain lag/fragmentation problems, and a low value with 'still busy' would explain tearing.")); - this->AddPage(_page_rendering, _("Rendering"), PREFS_PAGE_RENDERING); + AddPage(_page_rendering, _("Rendering"), PREFS_PAGE_RENDERING); } void InkscapePreferences::initPageBitmaps() diff --git a/src/ui/dialog/inkscape-preferences.h b/src/ui/dialog/inkscape-preferences.h index f73f3d74cd..82141a32a5 100644 --- a/src/ui/dialog/inkscape-preferences.h +++ b/src/ui/dialog/inkscape-preferences.h @@ -337,7 +337,6 @@ protected: UI::Widget::PrefSpinButton _filter_multi_threaded; UI::Widget::PrefSpinButton _rendering_cache_size; - UI::Widget::PrefSpinButton _rendering_tile_multiplier; UI::Widget::PrefSpinButton _rendering_xray_radius; UI::Widget::PrefSpinButton _rendering_outline_overlay_opacity; UI::Widget::PrefCombo _canvas_update_strategy; @@ -357,22 +356,20 @@ protected: #endif UI::Widget::PrefCheckButton _canvas_developer_mode_enabled; + UI::Widget::PrefSpinButton _canvas_tile_size; UI::Widget::PrefSpinButton _canvas_render_time_limit; - UI::Widget::PrefCheckButton _canvas_use_new_bisector; - UI::Widget::PrefSpinButton _canvas_new_bisector_size; - UI::Widget::PrefSpinButton _rendering_tile_size; - UI::Widget::PrefSpinButton _canvas_pad; - UI::Widget::PrefSpinButton _canvas_margin; + UI::Widget::PrefCheckButton _canvas_block_updates; + UI::Widget::PrefCombo _canvas_pixelstreamer_method; + UI::Widget::PrefSpinButton _canvas_padding; + UI::Widget::PrefSpinButton _canvas_prerender; UI::Widget::PrefSpinButton _canvas_preempt; UI::Widget::PrefSpinButton _canvas_coarsener_min_size; UI::Widget::PrefSpinButton _canvas_coarsener_glue_size; UI::Widget::PrefSpinButton _canvas_coarsener_min_fullness; - UI::Widget::PrefCombo _canvas_pixelstreamer_method; - UI::Widget::PrefCheckButton _canvas_block_updates; UI::Widget::PrefCheckButton _canvas_debug_framecheck; UI::Widget::PrefCheckButton _canvas_debug_logging; - UI::Widget::PrefCheckButton _canvas_debug_slow_redraw; - UI::Widget::PrefSpinButton _canvas_debug_slow_redraw_time; + UI::Widget::PrefCheckButton _canvas_debug_delay_redraw; + UI::Widget::PrefSpinButton _canvas_debug_delay_redraw_time; UI::Widget::PrefCheckButton _canvas_debug_show_redraw; UI::Widget::PrefCheckButton _canvas_debug_show_unclean; UI::Widget::PrefCheckButton _canvas_debug_show_snapshot; @@ -380,7 +377,6 @@ protected: UI::Widget::PrefCheckButton _canvas_debug_disable_redraw; UI::Widget::PrefCheckButton _canvas_debug_sticky_decoupled; UI::Widget::PrefCheckButton _canvas_debug_animate; - UI::Widget::PrefCheckButton _canvas_debug_idle_starvation; UI::Widget::PrefCheckButton _trans_scale_stroke; UI::Widget::PrefCheckButton _trans_scale_corner; diff --git a/src/ui/knot/knot.cpp b/src/ui/knot/knot.cpp index f024a732e5..c862e07325 100644 --- a/src/ui/knot/knot.cpp +++ b/src/ui/knot/knot.cpp @@ -341,7 +341,7 @@ void sp_knot_handler_request_position(GdkEvent *event, SPKnot *knot) { knot->desktop->set_coordinate_status(knot->pos); // display the coordinate of knot, not cursor - they may be different! if (event->motion.state & GDK_BUTTON1_MASK) { - knot->desktop->canvas->gobble_motion_events(GDK_BUTTON1_MASK); + Inkscape::UI::Tools::gobble_motion_events(GDK_BUTTON1_MASK); } } diff --git a/src/ui/tool/control-point-selection.cpp b/src/ui/tool/control-point-selection.cpp index 68525e6541..b94129c8d3 100644 --- a/src/ui/tool/control-point-selection.cpp +++ b/src/ui/tool/control-point-selection.cpp @@ -520,7 +520,7 @@ void ControlPointSelection::_updateTransformHandles(bool preserve_center) bool ControlPointSelection::_keyboardMove(GdkEventKey const &event, Geom::Point const &dir) { if (held_control(event)) return false; - unsigned num = 1 + _desktop->canvas->gobble_key_events(shortcut_key(event), 0); + unsigned num = 1 + Tools::gobble_key_events(shortcut_key(event), 0); Geom::Point delta = dir * num; if (held_shift(event)) delta *= 10; diff --git a/src/ui/tools/calligraphic-tool.cpp b/src/ui/tools/calligraphic-tool.cpp index 7cb631fd0a..adecd602a0 100644 --- a/src/ui/tools/calligraphic-tool.cpp +++ b/src/ui/tools/calligraphic-tool.cpp @@ -67,6 +67,7 @@ #include "ui/icon-names.h" #include "ui/tools/freehand-base.h" +#include "ui/widget/canvas.h" #include "util/units.h" @@ -325,8 +326,8 @@ void CalligraphicTool::brush() { Inkscape::CanvasItemDrawing *canvas_item_drawing = _desktop->getCanvasDrawing(); Inkscape::Drawing *drawing = canvas_item_drawing->get_drawing(); - // Ensure drawing up-to-date. (Is this really necessary?) - drawing->update(); + // Non-reentrancy workaround. + _desktop->canvas->wait_for_drawing_inactive(); // Get average color. double R, G, B, A; diff --git a/src/ui/tools/dropper-tool.cpp b/src/ui/tools/dropper-tool.cpp index 6fdcd0c3f4..721d962223 100644 --- a/src/ui/tools/dropper-tool.cpp +++ b/src/ui/tools/dropper-tool.cpp @@ -230,8 +230,8 @@ bool DropperTool::root_handler(GdkEvent* event) { Inkscape::CanvasItemDrawing *canvas_item_drawing = _desktop->getCanvasDrawing(); Inkscape::Drawing *drawing = canvas_item_drawing->get_drawing(); - // Ensure drawing up-to-date. (Is this really necessary?) - drawing->update(); + // Non-reentrancy workaround. + _desktop->canvas->wait_for_drawing_inactive(); // Get average color. double R, G, B, A; diff --git a/src/ui/tools/spray-tool.cpp b/src/ui/tools/spray-tool.cpp index 3396d36a65..a637e631d0 100644 --- a/src/ui/tools/spray-tool.cpp +++ b/src/ui/tools/spray-tool.cpp @@ -61,7 +61,7 @@ #include "ui/icon-names.h" #include "ui/toolbar/spray-toolbar.h" #include "ui/tools/spray-tool.h" - +#include "ui/widget/canvas.h" using Inkscape::DocumentUndo; @@ -425,8 +425,8 @@ static guint32 getPickerData(Geom::IntRect area, SPDesktop *desktop) Inkscape::CanvasItemDrawing *canvas_item_drawing = desktop->getCanvasDrawing(); Inkscape::Drawing *drawing = canvas_item_drawing->get_drawing(); - // Ensure drawing up-to-date. (Is this really necessary?) - drawing->update(); + // Non-reentrancy workaround. + desktop->canvas->wait_for_drawing_inactive(); // Get average color. double R, G, B, A; diff --git a/src/ui/tools/tool-base.cpp b/src/ui/tools/tool-base.cpp index 42da4d8dde..73945f7736 100644 --- a/src/ui/tools/tool-base.cpp +++ b/src/ui/tools/tool-base.cpp @@ -182,6 +182,52 @@ void ToolBase::use_cursor(Glib::RefPtr cursor) } } +/** + * Gobbles next key events on the queue with the same keyval and mask. Returns the number of events consumed. + */ +gint gobble_key_events(guint keyval, guint mask) { + GdkEvent *event_next; + gint i = 0; + + event_next = gdk_event_get(); + // while the next event is also a key notify with the same keyval and mask, + while (event_next && (event_next->type == GDK_KEY_PRESS || event_next->type + == GDK_KEY_RELEASE) && event_next->key.keyval == keyval && (!mask + || (event_next->key.state & mask))) { + if (event_next->type == GDK_KEY_PRESS) + i++; + // kill it + gdk_event_free(event_next); + // get next + event_next = gdk_event_get(); + } + // otherwise, put it back onto the queue + if (event_next) + gdk_event_put(event_next); + + return i; +} + +/** + * Gobbles next motion notify events on the queue with the same mask. Returns the number of events consumed. + */ +void gobble_motion_events(guint mask) { + GdkEvent *event_next; + + event_next = gdk_event_get(); + // while the next event is also a key notify with the same keyval and mask, + while (event_next && event_next->type == GDK_MOTION_NOTIFY + && (event_next->motion.state & mask)) { + // kill it + gdk_event_free(event_next); + // get next + event_next = gdk_event_get(); + } + // otherwise, put it back onto the queue + if (event_next) + gdk_event_put(event_next); +} + /** * Toggles current tool between active tool and selector tool. * Subroutine of sp_event_context_private_root_handler(). @@ -1108,7 +1154,9 @@ void ToolBase::ungrabCanvasEvents() * to draw a line). Make sure to call it again and restore standard precision afterwards. **/ void ToolBase::set_high_motion_precision(bool high_precision) { - _desktop->canvas->set_event_compression(!high_precision); + if (auto window = _desktop->getToplevel()->get_window()) { + window->set_event_compression(!high_precision); + } } Geom::Point ToolBase::setup_for_drag_start(GdkEvent *ev) @@ -1122,24 +1170,6 @@ Geom::Point ToolBase::setup_for_drag_start(GdkEvent *ev) return _desktop->w2d(p); } -/** - * Discard and count matching key events from top of event bucket. - * Convenience function that just passes request to canvas. - */ -int ToolBase::gobble_key_events(guint keyval, guint mask) const -{ - return _desktop->canvas->gobble_key_events(keyval, mask); -} - -/** - * Discard matching motion events from top of event bucket. - * Convenience function that just passes request to canvas. - */ -void ToolBase::gobble_motion_events(guint mask) const -{ - _desktop->canvas->gobble_motion_events(mask); -} - /** * Calls virtual set() function of ToolBase. */ diff --git a/src/ui/tools/tool-base.h b/src/ui/tools/tool-base.h index ced5ed0485..ffa6100f33 100644 --- a/src/ui/tools/tool-base.h +++ b/src/ui/tools/tool-base.h @@ -212,9 +212,6 @@ protected: void set_high_motion_precision(bool high_precision = true); - int gobble_key_events(guint keyval, guint mask) const; - void gobble_motion_events(guint mask) const; - SPDesktop *_desktop = nullptr; private: @@ -230,6 +227,9 @@ void sp_event_context_read(ToolBase *ec, char const *key); void sp_event_root_menu_popup(SPDesktop *desktop, SPItem *item, GdkEvent *event); +gint gobble_key_events(guint keyval, guint mask); +void gobble_motion_events(guint mask); + void sp_event_show_modifier_tip(Inkscape::MessageContext *message_context, GdkEvent *event, char const *ctrl_tip, char const *shift_tip, char const *alt_tip); diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index b152bda3f1..00b2334698 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -12,9 +12,13 @@ #include // Logging #include // Sort #include // Coarsener +#include +#include #include +#include +#include +#include #include <2geom/convex-hull.h> -#include #include "canvas.h" #include "canvas-grid.h" @@ -27,6 +31,12 @@ #include "ui/util.h" #include "helper/geom.h" +#include "canvas/prefs.h" +#include "canvas/fragment.h" +#include "canvas/util.h" +#include "canvas/stores.h" +#include "canvas/graphics.h" +#include "canvas/synchronizer.h" #include "display/drawing.h" #include "display/control/canvas-item-drawing.h" #include "display/control/canvas-item-group.h" @@ -34,12 +44,8 @@ #include "ui/tools/tool-base.h" // Default cursor -#include "canvas/prefs.h" -#include "canvas/stores.h" -#include "canvas/updaters.h" -#include "canvas/graphics.h" -#include "canvas/util.h" -#include "canvas/framecheck.h" +#include "canvas/updaters.h" // Update strategies +#include "canvas/framecheck.h" // For frame profiling #define framecheck_whole_function(D) \ auto framecheckobj = D->prefs.debug_framecheck ? FrameCheck::Event(__func__) : FrameCheck::Event(); @@ -82,9 +88,7 @@ * them "externally" (e.g. gradient CanvasItemCurves). */ -namespace Inkscape { -namespace UI { -namespace Widget { +namespace Inkscape::UI::Widget { namespace { /* @@ -96,8 +100,9 @@ struct GdkEventFreer {void operator()(GdkEvent *ev) const {gdk_event_free(ev);}} using GdkEventUniqPtr = std::unique_ptr; // Copies a GdkEvent, returning the result as a smart pointer. -auto make_unique_copy(const GdkEvent *ev) {return GdkEventUniqPtr(gdk_event_copy(ev));} +auto make_unique_copy(GdkEvent const *ev) { return GdkEventUniqPtr(gdk_event_copy(ev)); } +// Convert an integer received from preferences into an Updater enum. auto pref_to_updater(int index) { constexpr auto arr = std::array{Updater::Strategy::Responsive, @@ -107,6 +112,74 @@ auto pref_to_updater(int index) return arr[index - 1]; } +// Represents the raster data and location of an in-flight tile (one that is drawn, but not yet pasted into the stores). +struct Tile +{ + Fragment fragment; + Cairo::RefPtr surface; + Cairo::RefPtr outline_surface; +}; + +// The urgency with which the async redraw process should exit. +enum class AbortFlags : int +{ + None = 0, + Soft = 1, // exit if reached prerender phase + Hard = 2 // exit in any phase +}; + +// A copy of all the data the async redraw process needs access to, along with its internal state. +struct RedrawData +{ + // Data on what/how to draw. + Geom::IntPoint mouse_loc; + Geom::IntRect visible; + Fragment store; + bool decoupled_mode; + Cairo::RefPtr snapshot_drawn; + Geom::OptIntRect grabbed; + + // Saved prefs + int coarsener_min_size; + int coarsener_glue_size; + double coarsener_min_fullness; + int tile_size; + int preempt; + int margin; + std::optional redraw_delay; + int render_time_limit; + int numthreads; + bool background_in_stores_required; + uint64_t page, desk; + bool debug_framecheck; + bool debug_show_redraw; + + // State + std::mutex mutex; + gint64 start_time; + int phase; + Geom::OptIntRect vis_store; + + Geom::IntRect bounds; + Cairo::RefPtr clean; + bool interruptible; + bool preemptible; + std::vector rects; + + // Results + std::mutex tiles_mutex; + std::vector tiles; + bool timeoutflag; + + // Return comparison object for sorting rectangles by distance from mouse point. + auto getcmp() const + { + return [mouse_loc = mouse_loc] (Geom::IntRect const &a, Geom::IntRect const &b) { + return distSq(mouse_loc, a) > distSq(mouse_loc, b); + }; + } +}; + } // namespace /* @@ -137,80 +210,69 @@ public: Stores stores; void handle_stores_action(Stores::Action action); - // Update strategy; tracks the unclean region and decides how to redraw it. - std::unique_ptr updater; + // Invalidation + std::unique_ptr updater; // Tracks the unclean region and decides how to redraw it. + Cairo::RefPtr invalidated; // Buffers invalidations while the updater is in use by the background process. // Graphics state; holds all the graphics resources, including the drawn content. std::unique_ptr graphics; void activate_graphics(); void deactivate_graphics(); - // Event processor. Events that interact with the Canvas are buffered here until the start of the next frame. They are processed by a separate object so that deleting the Canvas mid-event can be done safely. - struct EventProcessor - { - std::vector events; - int pos; - GdkEvent *ignore = nullptr; - CanvasPrivate *canvasprivate; // Nulled on destruction. - bool in_processing = false; // For handling recursion due to nested GTK main loops. - bool compression = true; // Whether event compression is enabled. - void process(); - void compress(); - int gobble_key_events(guint keyval, guint mask); - void gobble_motion_events(guint mask); - }; - std::shared_ptr eventprocessor; // Usually held by CanvasPrivate, but temporarily also held by itself while processing so that it is not deleted mid-event. - bool add_to_bucket(const GdkEvent*); - bool process_bucketed_event(const GdkEvent*); + // Redraw process management. + bool redraw_active = false; + bool redraw_requested = false; + sigc::connection schedule_redraw_conn; + void schedule_redraw(); + void launch_redraw(); + void after_redraw(); + void commit_tiles(); + + // Event handling. + bool process_event(const GdkEvent*); bool pick_current_item(const GdkEvent*); bool emit_event(const GdkEvent*); Inkscape::CanvasItem *pre_scroll_grabbed_item; - // State for determining when to run event processor. - bool pending_draw = false; - sigc::connection bucket_emptier; - std::optional bucket_emptier_tick_callback; - void schedule_bucket_emptier(); - void disconnect_bucket_emptier_tick_callback(); - - // Idle system. The high priority idle ensures at least one idle cycle between add_idle and on_draw. - void add_idle(); - sigc::connection hipri_idle; - sigc::connection lopri_idle; - bool on_hipri_idle(); - bool on_lopri_idle(); - bool idle_running = false; - - // Content drawing - bool on_idle(); - void paint_rect(Geom::IntRect const &rect); - void paint_single_buffer(const Cairo::RefPtr &surface, const Geom::IntRect &rect, bool need_background, bool outline_pass); - std::optional old_bisector(const Geom::IntRect &rect); - std::optional new_bisector(const Geom::IntRect &rect); - bool outlines_required() const { return q->_split_mode != Inkscape::SplitMode::NORMAL || q->_render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY; } - + // Various state affecting what is drawn. uint32_t desk = 0xffffffff; // The background colour, with the alpha channel used to control checkerboard. uint32_t border = 0x000000ff; // The border colour, used only to control shadow colour. uint32_t page = 0xffffffff; // The page colour, also with alpha channel used to control checkerboard. + bool clip_to_page = false; // Whether to enable clip-to-page mode. + PageInfo pi; // The list of page rectangles. + std::optional calc_page_clip() const; // Union of the page rectangles if in clip-to-page mode, otherwise no clip. - bool outlines_enabled = false; int scale_factor = 1; // The device scale the stores are drawn at. - PageInfo pi; - bool background_in_stores = false; - bool require_background_in_stores() const { return !q->get_opengl_enabled() && SP_RGBA32_A_U(page) == 255 && SP_RGBA32_A_U(desk) == 255; } // Enable solid colour optimisation if both page and desk are solid (as opposed to checkerboard). + bool outlines_enabled = false; // Whether to enable the outline layer. + bool outlines_required() const { return q->_split_mode != Inkscape::SplitMode::NORMAL || q->_render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY; } + + bool background_in_stores_enabled = false; // Whether the page and desk should be drawn into the stores/tiles; if not then transparency is used instead. + bool background_in_stores_required() const { return !q->get_opengl_enabled() && SP_RGBA32_A_U(page) == 255 && SP_RGBA32_A_U(desk) == 255; } // Enable solid colour optimisation if both page and desk are solid (as opposed to checkerboard). + + // Async redraw process. + std::optional pool; + int get_numthreads() const; + + Synchronizer sync; + Glib::Dispatcher commit_tiles_dispatcher; + RedrawData rd; + std::atomic abort_flags; + + void init_tiler(); + bool init_redraw(); + bool end_redraw(); // returns true to indicate further redraw cycles required + void process_redraw(Geom::IntRect const &bounds, Cairo::RefPtr clean, bool interruptible = true, bool preemptible = true); + void render_tile(int debug_id); + void paint_rect(Geom::IntRect const &rect); + void paint_single_buffer(const Cairo::RefPtr &surface, const Geom::IntRect &rect, bool need_background, bool outline_pass); // Trivial overload of GtkWidget function. - void queue_draw_area(const Geom::IntRect &rect); + void queue_draw_area(Geom::IntRect const &rect); // For tracking the last known mouse position. (The function Gdk::Window::get_device_position cannot be used because of slow X11 round-trips. Remove this workaround when X11 dies.) std::optional last_mouse; - - // Idle time starvation counter. - gint64 sample_begin = 0; - gint64 wait_begin = 0; - gint64 wait_accumulated = 0; }; /* @@ -234,26 +296,18 @@ Canvas::Canvas() Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK ); - // Set up EventProcessor - d->eventprocessor = std::make_shared(); - d->eventprocessor->canvasprivate = d.get(); - // Updater d->updater = Updater::create(pref_to_updater(d->prefs.update_strategy)); d->updater->reset(); + d->invalidated = Cairo::Region::create(); // Preferences d->prefs.grabsize.action = [=] { d->canvasitem_ctx->root()->update_canvas_item_ctrl_sizes(d->prefs.grabsize); }; d->prefs.debug_show_unclean.action = [=] { queue_draw(); }; d->prefs.debug_show_clean.action = [=] { queue_draw(); }; - d->prefs.debug_disable_redraw.action = [=] { d->add_idle(); }; - d->prefs.debug_sticky_decoupled.action = [=] { d->add_idle(); }; + d->prefs.debug_disable_redraw.action = [=] { d->schedule_redraw(); }; + d->prefs.debug_sticky_decoupled.action = [=] { d->schedule_redraw(); }; d->prefs.debug_animate.action = [=] { queue_draw(); }; - d->prefs.update_strategy.action = [=] { - auto new_updater = Updater::create(pref_to_updater(d->prefs.update_strategy)); - new_updater->clean_region = std::move(d->updater->clean_region); - d->updater = std::move(new_updater); - }; d->prefs.outline_overlay_opacity.action = [=] { queue_draw(); }; d->prefs.softproof.action = [=] { redraw_all(); }; d->prefs.displayprofile.action = [=] { redraw_all(); }; @@ -275,7 +329,6 @@ Canvas::Canvas() d->activate(); } }; - d->prefs.debug_idle_starvation.action = [=] { d->sample_begin = d->wait_begin = d->wait_accumulated = 0; }; // Canvas item tree d->canvasitem_ctx.emplace(this); @@ -285,10 +338,37 @@ Canvas::Canvas() _split_frac = {0.5, 0.5}; // Recreate stores on HiDPI change. - property_scale_factor().signal_changed().connect([this] { d->add_idle(); }); + property_scale_factor().signal_changed().connect([this] { d->schedule_redraw(); }); // OpenGL switch. set_opengl_enabled(d->prefs.request_opengl); + + // Async redraw process. + d->pool.emplace(1); + + d->sync.connectExit([this] { d->after_redraw(); }); + + d->commit_tiles_dispatcher.connect([this] { + if (get_opengl_enabled()) make_current(); + d->commit_tiles(); + }); +} + +int CanvasPrivate::get_numthreads() const +{ + // Todo: Remove this when rendering is reentrant. + return 1; + + if (int n = prefs.numthreads; n > 0) { + // First choice is the value set in preferences. + return n; + } else if (int n = std::thread::hardware_concurrency(); n > 0) { + // If not set, use the number of processors. + return n; + } else { + // If not reported, use a sensible fallback. + return 4; + } } // Graphics becomes active when the widget is realized. @@ -332,36 +412,49 @@ void CanvasPrivate::activate() q->_split_dragging = false; // Todo: Disable GTK event compression again when doing so is no longer buggy. - q->get_window()->set_event_compression(true); + // Note: ToolBase::set_high_motion_precision() will keep turning it back on. + // q->get_window()->set_event_compression(false); active = true; - add_idle(); + schedule_redraw(); } void CanvasPrivate::deactivate() { active = false; - // Disconnect signals and timeouts. (Note: They will never be rescheduled while inactive.) - hipri_idle.disconnect(); - lopri_idle.disconnect(); - bucket_emptier.disconnect(); - disconnect_bucket_emptier_tick_callback(); + if (redraw_active) { + if (schedule_redraw_conn.connected()) { + // In first link in chain, from schedule_redraw() to launch_redraw(). Break the link and exit. + schedule_redraw_conn.disconnect(); + } else { + // Otherwise, the background process is running. Interrupt the signal chain at exit. + abort_flags.store((int)AbortFlags::Hard, std::memory_order_relaxed); + if (prefs.debug_logging) std::cout << "Hard exit request" << std::endl; + sync.waitForExit(); + + // Unsnapshot the CanvasItems and DrawingItems. + canvasitem_ctx->unsnapshot(); + q->_drawing->unsnapshot(); + } + + redraw_active = false; + redraw_requested = false; + assert(!schedule_redraw_conn.connected()); + } } void CanvasPrivate::deactivate_graphics() { - stores.set_graphics(nullptr); if (q->get_opengl_enabled()) q->make_current(); + commit_tiles(); + stores.set_graphics(nullptr); graphics.reset(); } Canvas::~Canvas() { - // Disconnect from EventProcessor. - d->eventprocessor->canvasprivate = nullptr; - // Remove entire CanvasItem tree. d->canvasitem_ctx.reset(); } @@ -398,16 +491,281 @@ void Canvas::on_unrealize() } /* - * Events system + * Redraw process managment */ -// The following protected functions of Canvas are where all incoming events initially arrive. -// Those that do not interact with the Canvas are processed instantaneously, while the rest are -// delayed by placing them into the bucket. +// Schedule another redraw iteration to take place, waiting for the current one to finish if necessary. +void CanvasPrivate::schedule_redraw() +{ + if (!active) { + // We can safely discard calls until active, because we will run an iteration on activation later in initialisation. + return; + } + + // Ensure another iteration is performed if one is in progress. + redraw_requested = true; + + if (redraw_active) { + return; + } + + redraw_active = true; + + // Call run_redraw() as soon as possible on the main loop. (Cannot run now since CanvasItem tree could be in an invalid intermediate state.) + assert(!schedule_redraw_conn.connected()); + schedule_redraw_conn = Glib::signal_idle().connect([this] { + if (q->get_opengl_enabled()) { + q->make_current(); + } + if (prefs.debug_logging) std::cout << "Redraw start" << std::endl; + launch_redraw(); + return false; + }, Glib::PRIORITY_HIGH); +} + +// Update state and launch redraw process in background. Requires a current OpenGL context. +void CanvasPrivate::launch_redraw() +{ + assert(redraw_active); + + // Determine whether the rendering parameters have changed, and trigger full store recreation if so. + if ((outlines_required() && !outlines_enabled) || scale_factor != q->get_scale_factor()) { + stores.reset(); + } + + outlines_enabled = outlines_required(); + scale_factor = q->get_scale_factor(); + + graphics->set_outlines_enabled(outlines_enabled); + graphics->set_scale_factor(scale_factor); + + /* + * Update state. + */ + + // Page information. + pi.pages.clear(); + canvasitem_ctx->root()->visit_page_rects([this] (auto &rect) { + pi.pages.emplace_back(rect); + }); + + graphics->set_colours(page, desk, border); + graphics->set_background_in_stores(background_in_stores_required()); + + q->_drawing->setClip(calc_page_clip()); + + // Stores. + handle_stores_action(stores.update(Fragment{ q->_affine, q->get_area_world() })); + + // Geometry. + bool const affine_changed = canvasitem_ctx->affine() != stores.store().affine; + if (q->_need_update || affine_changed) { + FrameCheck::Event fc; + if (prefs.debug_framecheck) fc = FrameCheck::Event("update"); + q->_need_update = false; + canvasitem_ctx->setAffine(stores.store().affine); + canvasitem_ctx->root()->update(affine_changed); + } + + // Update strategy. + auto const strategy = pref_to_updater(prefs.update_strategy); + if (updater->get_strategy() != strategy) { + auto new_updater = Updater::create(strategy); + new_updater->clean_region = std::move(updater->clean_region); + updater = std::move(new_updater); + } + + updater->mark_dirty(invalidated); + invalidated = Cairo::Region::create(); + + updater->next_frame(); + + /* + * Launch redraw process in background. + */ + + // If asked to, don't paint anything and instead halt the redraw process. + if (prefs.debug_disable_redraw) { + redraw_active = false; + return; + } + + // Snapshot the CanvasItems and DrawingItems. + canvasitem_ctx->snapshot(); + q->_drawing->snapshot(); + + // Get the mouse position in screen space. + rd.mouse_loc = last_mouse.value_or((Geom::Point(q->get_dimensions()) / 2).round()); + + // Map the mouse to canvas space. + rd.mouse_loc += q->_pos; + if (stores.mode() == Stores::Mode::Decoupled) { + rd.mouse_loc = (Geom::Point(rd.mouse_loc) * q->_affine.inverse() * stores.store().affine).round(); + } + + // Get the visible rect. + rd.visible = q->get_area_world(); + if (stores.mode() == Stores::Mode::Decoupled) { + rd.visible = (Geom::Parallelogram(rd.visible) * q->_affine.inverse() * stores.store().affine).bounds().roundOutwards(); + } + + // Get other misc data. + rd.store = Fragment{ stores.store().affine, stores.store().rect }; + rd.decoupled_mode = stores.mode() == Stores::Mode::Decoupled; + rd.coarsener_min_size = prefs.coarsener_min_size; + rd.coarsener_glue_size = prefs.coarsener_glue_size; + rd.coarsener_min_fullness = prefs.coarsener_min_fullness; + rd.tile_size = prefs.tile_size; + rd.preempt = prefs.preempt; + rd.margin = prefs.prerender; + rd.redraw_delay = prefs.debug_delay_redraw ? std::make_optional(prefs.debug_delay_redraw_time) : std::nullopt; + rd.render_time_limit = prefs.render_time_limit; + rd.numthreads = get_numthreads(); + rd.background_in_stores_required = background_in_stores_required(); + rd.page = page; + rd.desk = desk; + rd.debug_framecheck = prefs.debug_framecheck; + rd.debug_show_redraw = prefs.debug_show_redraw; + + rd.snapshot_drawn = stores.snapshot().drawn ? stores.snapshot().drawn->copy() : Cairo::RefPtr(); + rd.grabbed = q->_grabbed_canvas_item && prefs.block_updates ? regularised(roundedOutwards(q->_grabbed_canvas_item->get_bounds()) & rd.visible & rd.store.rect) : Geom::OptIntRect(); + + abort_flags.store((int)AbortFlags::None, std::memory_order_relaxed); + + boost::asio::post(*pool, [this] { init_tiler(); }); +} + +void CanvasPrivate::after_redraw() +{ + assert(redraw_active); + + // Unsnapshot the CanvasItems and DrawingItems. + canvasitem_ctx->unsnapshot(); + q->_drawing->unsnapshot(); + + // OpenGL context needed for commit_tiles(), stores.finished_draw(), and launch_redraw(). + if (q->get_opengl_enabled()) { + q->make_current(); + } + + // Commit tiles before stores.finished_draw() to avoid changing stores while tiles are still pending. + commit_tiles(); + + // Handle any pending stores action. + bool stores_changed = false; + if (!rd.timeoutflag) { + auto const ret = stores.finished_draw(Fragment{ q->_affine, q->get_area_world() }); + handle_stores_action(ret); + if (ret != Stores::Action::None) { + stores_changed = true; + } + } + + // Relaunch or stop as necessary. + if (rd.timeoutflag || redraw_requested || stores_changed) { + if (prefs.debug_logging) std::cout << "Continuing redrawing" << std::endl; + redraw_requested = false; + launch_redraw(); + } else { + if (prefs.debug_logging) std::cout << "Redraw exit" << std::endl; + redraw_active = false; + } +} + +void CanvasPrivate::handle_stores_action(Stores::Action action) +{ + switch (action) { + case Stores::Action::Recreated: + // Set everything as needing redraw. + invalidated->do_union(geom_to_cairo(stores.store().rect)); + updater->reset(); + + if (prefs.debug_show_unclean) q->queue_draw(); + break; + + case Stores::Action::Shifted: + invalidated->intersect(geom_to_cairo(stores.store().rect)); + updater->intersect(stores.store().rect); + + if (prefs.debug_show_unclean) q->queue_draw(); + break; + + default: + break; + } + + if (action != Stores::Action::None) { + auto expanded = stores.store().rect; + auto expansion = Geom::IntPoint(expanded.width() / 2, expanded.height() / 2); + expanded.expandBy(expansion); + q->_drawing->setCacheLimit(expanded); + } +} + +// Commit all in-flight tiles to the stores. Requires a current OpenGL context (for graphics->draw_tile). +void CanvasPrivate::commit_tiles() +{ + decltype(rd.tiles) tiles; + + { + auto lock = std::lock_guard(rd.tiles_mutex); + tiles = std::move(rd.tiles); + } + + for (auto &tile : tiles) { + // Todo: Make CMS system thread-safe, then move this to render thread too. + if (q->_cms_active) { + auto transf = prefs.from_display + ? Inkscape::CMSSystem::getDisplayPer(q->_cms_key) + : Inkscape::CMSSystem::getDisplayTransform(); + if (transf) { + tile.surface->flush(); + auto px = tile.surface->get_data(); + int stride = tile.surface->get_stride(); + for (int i = 0; i < tile.surface->get_height(); i++) { + auto row = px + i * stride; + Inkscape::CMSSystem::doTransform(transf, row, row, tile.surface->get_width()); + } + tile.surface->mark_dirty(); + } + } + + // Paste tile content onto stores. + graphics->draw_tile(tile.fragment, std::move(tile.surface), std::move(tile.outline_surface)); + + // Add to drawn region. + assert(stores.store().rect.contains(tile.fragment.rect)); + stores.mark_drawn(tile.fragment.rect); + + // Get the rectangle of screen-space needing repaint. + Geom::IntRect repaint_rect; + if (stores.mode() == Stores::Mode::Normal) { + // Simply translate to get back to screen space. + repaint_rect = tile.fragment.rect - q->_pos; + } else { + // Transform into screen space, take bounding box, and round outwards. + auto pl = Geom::Parallelogram(tile.fragment.rect); + pl *= stores.store().affine.inverse() * q->_affine; + pl *= Geom::Translate(-q->_pos); + repaint_rect = pl.bounds().roundOutwards(); + } + + // Check if repaint is necessary - some rectangles could be entirely off-screen. + auto screen_rect = Geom::IntRect({0, 0}, q->get_dimensions()); + if (regularised(repaint_rect & screen_rect)) { + // Schedule repaint. + queue_draw_area(repaint_rect); + } + } +} + +/* + * Event handling + */ bool Canvas::on_scroll_event(GdkEventScroll *scroll_event) { - return d->add_to_bucket(reinterpret_cast(scroll_event)); + return d->process_event(reinterpret_cast(scroll_event)); } bool Canvas::on_button_press_event(GdkEventButton *button_event) @@ -455,14 +813,15 @@ bool Canvas::on_button_event(GdkEventButton *button_event) } break; case GDK_BUTTON_RELEASE: + if (!_split_dragging) break; _split_dragging = false; // Check if we are near the edge. If so, revert to normal mode. - if (cursor_position.x() < 5 || - cursor_position.y() < 5 || - cursor_position.x() - get_allocation().get_width() > -5 || - cursor_position.y() - get_allocation().get_height() > -5 ) { - + if (cursor_position.x() < 5 || + cursor_position.y() < 5 || + cursor_position.x() > get_allocation().get_width() - 5 || + cursor_position.y() > get_allocation().get_height() - 5) + { // Reset everything. _split_frac = {0.5, 0.5}; set_cursor(); @@ -487,7 +846,7 @@ bool Canvas::on_button_event(GdkEventButton *button_event) return true; } - saction->change_state((int)Inkscape::SplitMode::NORMAL); + saction->change_state(static_cast(Inkscape::SplitMode::NORMAL)); } break; @@ -497,8 +856,7 @@ bool Canvas::on_button_event(GdkEventButton *button_event) } } - // Otherwise, handle as a delayed event. - return d->add_to_bucket(reinterpret_cast(button_event)); + return d->process_event(reinterpret_cast(button_event)); } bool Canvas::on_enter_notify_event(GdkEventCrossing *crossing_event) @@ -506,7 +864,7 @@ bool Canvas::on_enter_notify_event(GdkEventCrossing *crossing_event) if (crossing_event->window != get_window()->gobj()) { return false; } - return d->add_to_bucket(reinterpret_cast(crossing_event)); + return d->process_event(reinterpret_cast(crossing_event)); } bool Canvas::on_leave_notify_event(GdkEventCrossing *crossing_event) @@ -515,7 +873,7 @@ bool Canvas::on_leave_notify_event(GdkEventCrossing *crossing_event) return false; } d->last_mouse = {}; - return d->add_to_bucket(reinterpret_cast(crossing_event)); + return d->process_event(reinterpret_cast(crossing_event)); } bool Canvas::on_focus_in_event(GdkEventFocus *focus_event) @@ -526,12 +884,12 @@ bool Canvas::on_focus_in_event(GdkEventFocus *focus_event) bool Canvas::on_key_press_event(GdkEventKey *key_event) { - return d->add_to_bucket(reinterpret_cast(key_event)); + return d->process_event(reinterpret_cast(key_event)); } bool Canvas::on_key_release_event(GdkEventKey *key_event) { - return d->add_to_bucket(reinterpret_cast(key_event)); + return d->process_event(reinterpret_cast(key_event)); } bool Canvas::on_motion_notify_event(GdkEventMotion *motion_event) @@ -578,14 +936,15 @@ bool Canvas::on_motion_notify_event(GdkEventMotion *motion_event) } } } else if (_split_direction == Inkscape::SplitDirection::NORTH || - _split_direction == Inkscape::SplitDirection::SOUTH) { + _split_direction == Inkscape::SplitDirection::SOUTH) + { if (std::abs(diff.y()) < 3) { - // We're hovering over horizontal line + // We're hovering over the horizontal line. hover_direction = Inkscape::SplitDirection::HORIZONTAL; } } else { if (std::abs(diff.x()) < 3) { - // We're hovering over vertical line + // We're hovering over the vertical line. hover_direction = Inkscape::SplitDirection::VERTICAL; } } @@ -602,219 +961,19 @@ bool Canvas::on_motion_notify_event(GdkEventMotion *motion_event) } } - // Otherwise, handle as a delayed event. - return d->add_to_bucket(reinterpret_cast(motion_event)); + return d->process_event(reinterpret_cast(motion_event)); } -// Most events end up here. We store them in the bucket, and process them as soon as possible after -// the next 'on_draw'. If 'on_draw' isn't pending, we use the 'tick_callback' signal to process them -// when 'on_draw' would have run anyway. If 'on_draw' later becomes pending, we remove this signal. - -// Add an event to the bucket and ensure it will be emptied in the near future. -bool CanvasPrivate::add_to_bucket(const GdkEvent *event) +// Unified handler for all events. +bool CanvasPrivate::process_event(const GdkEvent *event) { framecheck_whole_function(this) if (!active) { - std::cerr << "Canvas::add_to_bucket: Called while not active!" << std::endl; - return false; - } - - // Prevent re-fired events from going through again. - if (event == eventprocessor->ignore) { + std::cerr << "Canvas::process_event: Called while not active!" << std::endl; return false; } - // If this is the first event, ensure event processing will run on the main loop as soon as possible after the next frame has started. - if (eventprocessor->events.empty() && !pending_draw) { - assert(!bucket_emptier_tick_callback); // Guaranteed since cleared when the event queue is emptied and not set until non-empty again. - bucket_emptier_tick_callback = q->add_tick_callback([this] (const Glib::RefPtr&) { - assert(active); // Guaranteed since disconnected upon becoming inactive and not scheduled until active again. - bucket_emptier_tick_callback.reset(); - schedule_bucket_emptier(); - return false; - }); - } - - // Add a copy to the queue. - eventprocessor->events.emplace_back(gdk_event_copy(event)); - - // Tell GTK the event was handled. - return true; -} - -void CanvasPrivate::schedule_bucket_emptier() -{ - if (!active) { - std::cerr << "Canvas::schedule_bucket_emptier: Called while not active!" << std::endl; - return; - } - - if (!bucket_emptier.connected()) { - bucket_emptier = Glib::signal_idle().connect([this] { - assert(active); - eventprocessor->process(); - return false; - }, G_PRIORITY_HIGH_IDLE + 14); // before hipri_idle - } -} - -void CanvasPrivate::disconnect_bucket_emptier_tick_callback() -{ - if (bucket_emptier_tick_callback) { - q->remove_tick_callback(*bucket_emptier_tick_callback); - bucket_emptier_tick_callback.reset(); - } -} - -// The following functions run at the start of the next frame on the GTK main loop. -// (Note: It is crucial that it runs on the main loop and not in any frame clock tick callbacks. GTK does not allow widgets to be deleted in the latter; only the former.) - -// Process bucketed events. -void CanvasPrivate::EventProcessor::process() -{ - framecheck_whole_function(canvasprivate) - - // Ensure the EventProcessor continues to live even if the Canvas is destroyed during event processing. - auto self = canvasprivate->eventprocessor; - - // Check if toplevel or recursive. (Recursive calls happen if processing an event starts its own nested GTK main loop.) - bool toplevel = !in_processing; - in_processing = true; - - // If toplevel, run compression, and initialise the iteration index. It may be incremented externally by gobblers or recursive calls. - if (toplevel) { - if (compression) compress(); - pos = 0; - } - - while (pos < events.size()) { - // Extract next event. - auto event = std::move(events[pos]); - pos++; - - // Fire the event at the CanvasItems and see if it was handled. - bool handled = canvasprivate->process_bucketed_event(event.get()); - - if (!handled) { - // Re-fire the event at the window, and ignore it when it comes back here again. - ignore = event.get(); - canvasprivate->q->get_toplevel()->event(event.get()); - ignore = nullptr; - } - - // If the Canvas was destroyed or deactivated during event processing, exit now. - if (!canvasprivate || !canvasprivate->active) return; - } - - // Otherwise, clear the list of events that was just processed. - events.clear(); - - // Disconnect the bucket emptier tick callback, as no longer anything to empty. - canvasprivate->disconnect_bucket_emptier_tick_callback(); - - // Reset the variable to track recursive calls. - if (toplevel) { - in_processing = false; - } -} - -// Called before event processing starts to perform event compression. -void CanvasPrivate::EventProcessor::compress() -{ - int in = 0, out = 0; - - while (in < events.size()) { - // Compress motion events belonging to the same device. - if (events[in]->type == GDK_MOTION_NOTIFY) { - auto begin = in, end = in + 1; - while (end < events.size() && events[end]->type == GDK_MOTION_NOTIFY && events[end]->motion.device == events[begin]->motion.device) end++; - // Check if there is more than one event to compress. - if (end != begin + 1) { - // Keep only the last event. - events[out] = std::move(events[end - 1]); - in = end; - out++; - continue; - } - } - - // Todo: Could consider compressing other events too (e.g. scrolls) if it helps. - - // Otherwise, leave the event untouched. - if (in != out) events[out] = std::move(events[in]); - in++; - out++; - } - - events.resize(out); -} - -void Canvas::set_event_compression(bool enabled) -{ - d->eventprocessor->compression = enabled; -} - -// Called during event processing by some tools to batch backlogs of key events that may have built up after a freeze. -int Canvas::gobble_key_events(guint keyval, guint mask) -{ - return d->eventprocessor->gobble_key_events(keyval, mask); -} - -int CanvasPrivate::EventProcessor::gobble_key_events(guint keyval, guint mask) -{ - int count = 0; - - while (pos < events.size()) { - auto &event = events[pos]; - if ((event->type == GDK_KEY_PRESS || event->type == GDK_KEY_RELEASE) && event->key.keyval == keyval && (!mask || (event->key.state & mask))) { - // Discard event and continue. - if (event->type == GDK_KEY_PRESS) count++; - pos++; - } else { - // Stop discarding. - break; - } - } - - if (count > 0 && canvasprivate->prefs.debug_logging) std::cout << "Gobbled " << count << " key press(es)" << std::endl; - - return count; -} - -// Called during event processing by some tools to ignore backlogs of motion events that may have built up after a freeze. -// Todo: Largely obviated since the introduction of event compression. May be possible to remove. -void Canvas::gobble_motion_events(guint mask) -{ - d->eventprocessor->gobble_motion_events(mask); -} - -void CanvasPrivate::EventProcessor::gobble_motion_events(guint mask) -{ - int count = 0; - - while (pos < events.size()) { - auto &event = events[pos]; - if (event->type == GDK_MOTION_NOTIFY && (event->motion.state & mask)) { - // Discard event and continue. - count++; - pos++; - } else { - // Stop discarding. - break; - } - } - - if (count > 0 && canvasprivate->prefs.debug_logging) std::cout << "Gobbled " << count << " motion event(s)" << std::endl; -} - -// From now on Inkscape's regular event processing logic takes place. The only thing to remember is that -// all of this happens at a slight delay after the original GTK events. Therefore, it's important to make -// sure that stateful variables like '_current_canvas_item' and friends are ONLY read/written within these -// functions, not during the earlier GTK event handlers. Otherwise state confusion will ensue. - -bool CanvasPrivate::process_bucketed_event(const GdkEvent *event) -{ auto calc_button_mask = [&] () -> int { switch (event->button.button) { case 1: return GDK_BUTTON1_MASK; break; @@ -860,9 +1019,7 @@ bool CanvasPrivate::process_bucketed_event(const GdkEvent *event) // ...then process the event. q->_state ^= calc_button_mask(); - bool retval = emit_event(event); - - return retval; + return emit_event(event); } case GDK_BUTTON_RELEASE: @@ -911,11 +1068,10 @@ bool CanvasPrivate::process_bucketed_event(const GdkEvent *event) } } -// This function is called by 'process_bucketed_event' to manipulate the state variables relating +// This function is called by 'process_event' to manipulate the state variables relating // to the current object under the mouse, for example, to generate enter and leave events. -// (A more detailed explanation by Tavmjong follows.) -// -------- -// This routine reacts to events from the canvas. It's main purpose is to find the canvas item +// +// This routine reacts to events from the canvas. Its main purpose is to find the canvas item // closest to the cursor where the event occurred and then send the event (sometimes modified) to // that item. The event then bubbles up the canvas item tree until an object handles it. If the // widget is redrawn, this routine may be called again for the same event. @@ -1224,21 +1380,6 @@ std::optional Canvas::get_last_mouse() const return d->last_mouse; } -/** - * Set the affine for the canvas. - */ -void Canvas::set_affine(Geom::Affine const &affine) -{ - if (_affine == affine) { - return; - } - - _affine = affine; - - d->add_idle(); - queue_draw(); -} - const Geom::Affine &Canvas::get_geom_affine() const { return d->canvasitem_ctx->affine(); @@ -1248,7 +1389,7 @@ void CanvasPrivate::queue_draw_area(const Geom::IntRect &rect) { if (q->get_opengl_enabled()) { // Note: GTK glitches out when you use queue_draw_area in OpenGL mode. - // Also, does GTK actually obey this command, or redraw the whole window? + // It's also pointless, because it seems to just call queue_draw anyway. q->queue_draw(); } else { q->queue_draw_area(rect.left(), rect.top(), rect.width(), rect.height()); @@ -1265,8 +1406,8 @@ void Canvas::redraw_all() // We need to ignore their requests! return; } - d->updater->reset(); // Empty region (i.e. everything is dirty). - d->add_idle(); + d->invalidated->do_union(geom_to_cairo(d->stores.store().rect)); + d->schedule_redraw(); if (d->prefs.debug_show_unclean) queue_draw(); } @@ -1295,9 +1436,14 @@ void Canvas::redraw_area(int x0, int y0, int x1, int y1) return; } - auto rect = Geom::IntRect::from_xywh(x0, y0, x1 - x0, y1 - y0); - d->updater->mark_dirty(rect); - d->add_idle(); + if (d->redraw_active && d->invalidated->empty()) { + d->abort_flags.store((int)AbortFlags::Soft, std::memory_order_relaxed); // responding to partial invalidations takes priority over prerendering + if (d->prefs.debug_logging) std::cout << "Soft exit request" << std::endl; + } + + auto const rect = Geom::IntRect(x0, y0, x1, y1); + d->invalidated->do_union(geom_to_cairo(rect)); + d->schedule_redraw(); if (d->prefs.debug_show_unclean) queue_draw(); } @@ -1316,7 +1462,7 @@ void Canvas::redraw_area(Geom::Coord x0, Geom::Coord y0, Geom::Coord x1, Geom::C ); } -void Canvas::redraw_area(Geom::Rect &area) +void Canvas::redraw_area(Geom::Rect const &area) { redraw_area(area.left(), area.top(), area.right(), area.bottom()); } @@ -1329,8 +1475,8 @@ void Canvas::request_update() // Flag geometry as needing update. _need_update = true; - // Trigger the idle process to perform the update. - d->add_idle(); + // Trigger the redraw process to perform the update. + d->schedule_redraw(); } /** @@ -1344,12 +1490,31 @@ void Canvas::set_pos(Geom::IntPoint const &pos) _pos = pos; - d->add_idle(); + if (auto grid = dynamic_cast(get_parent())) { + grid->UpdateRulers(); + } + + d->schedule_redraw(); queue_draw(); +} + +/** + * Set the affine for the canvas. + */ +void Canvas::set_affine(Geom::Affine const &affine) +{ + if (_affine == affine) { + return; + } + + _affine = affine; if (auto grid = dynamic_cast(get_parent())) { grid->UpdateRulers(); } + + d->schedule_redraw(); + queue_draw(); } /** @@ -1358,9 +1523,9 @@ void Canvas::set_pos(Geom::IntPoint const &pos) void Canvas::set_desk(uint32_t rgba) { if (d->desk == rgba) return; - bool invalidated = d->background_in_stores; + bool invalidated = d->background_in_stores_enabled; d->desk = rgba; - invalidated |= d->background_in_stores = d->require_background_in_stores(); + invalidated |= d->background_in_stores_enabled = d->background_in_stores_required(); if (get_realized() && invalidated) redraw_all(); queue_draw(); } @@ -1381,9 +1546,9 @@ void Canvas::set_border(uint32_t rgba) void Canvas::set_page(uint32_t rgba) { if (d->page == rgba) return; - bool invalidated = d->background_in_stores; + bool invalidated = d->background_in_stores_enabled; d->page = rgba; - invalidated |= d->background_in_stores = d->require_background_in_stores(); + invalidated |= d->background_in_stores_enabled = d->background_in_stores_required(); if (get_realized() && invalidated) redraw_all(); queue_draw(); } @@ -1398,7 +1563,7 @@ void Canvas::set_drawing_disabled(bool disable) { _drawing_disabled = disable; if (!disable) { - d->add_idle(); + d->schedule_redraw(); } } @@ -1446,7 +1611,7 @@ void Canvas::set_clip_to_page_mode(bool clip) { if (clip != d->clip_to_page) { d->clip_to_page = clip; - d->add_idle(); + d->schedule_redraw(); } } @@ -1482,6 +1647,34 @@ void Canvas::canvas_item_destructed(Inkscape::CanvasItem *item) } } +void Canvas::wait_for_drawing_inactive() const +{ + if (d->redraw_active && !d->schedule_redraw_conn.connected()) { + gint64 start; + if (d->prefs.debug_logging) start = g_get_monotonic_time(); + + // Background process is running. Wait for it to stop. + d->sync.waitForExit(); + // Continue the interrupted signal chain, reactivating drawing later on the main loop. + d->sync.signalExit(); + + if (d->prefs.debug_logging) std::cout << "Waited for inactivity: " << g_get_monotonic_time() - start << " μs" << std::endl; + } +} + +std::optional CanvasPrivate::calc_page_clip() const +{ + if (!clip_to_page) { + return {}; + } + + Geom::PathVector pv; + for (auto &rect : pi.pages) { + pv.push_back(Geom::Path(rect)); + } + return pv; +} + // Change cursor void Canvas::set_cursor() { @@ -1547,7 +1740,7 @@ void Canvas::on_size_allocate(Gtk::Allocation &allocation) } // Trigger the size update to be applied to the stores before the next redraw of the window. - d->add_idle(); + d->schedule_redraw(); } Glib::RefPtr Canvas::create_context() @@ -1571,12 +1764,10 @@ Glib::RefPtr Canvas::create_context() return result; } -void Canvas::paint_widget(const Cairo::RefPtr &cr) +void Canvas::paint_widget(Cairo::RefPtr const &cr) { framecheck_whole_function(d) - if (d->prefs.debug_idle_starvation && d->idle_running) d->wait_accumulated += g_get_monotonic_time() - d->wait_begin; - if (!d->active) { std::cerr << "Canvas::paint_widget: Called while not active!" << std::endl; return; @@ -1584,19 +1775,19 @@ void Canvas::paint_widget(const Cairo::RefPtr &cr) // _canvas_item_root->canvas_item_print_tree(); - // Although hipri_idle is scheduled at a priority higher than draw, and should therefore always be called first if - // asked, there are times when GTK simply decides to call on_draw anyway. Here we ensure that that call has taken - // place. This is problematic because if hipri_idle does rendering, enlarging the damage rect, then our drawing will - // still be clipped to the old damage rect. It was precisely this problem that lead to the introduction of - // hipri_idle. Fortunately, the following failsafe only seems to execute once during initialisation, and once on - // further resize events. Both these events seem to trigger a full damage, hence we are ok. - if (d->hipri_idle.connected()) { - d->hipri_idle.disconnect(); - d->on_hipri_idle(); + // Although launch_redraw() is scheduled at a priority higher than draw, and should therefore always be called first if + // asked, there are times when GTK simply decides to call on_draw anyway. Since launch_redraw() is required to have been + // called at least once to perform vital initalisation, if it has not been called, we have to exit. + if (d->stores.mode() == Stores::Mode::None) { + return; + } + + // Commit pending tiles in case GTK called on_draw even though after_redraw() is scheduled at higher priority. + if (!d->redraw_active) { + d->commit_tiles(); } if (get_opengl_enabled()) { - // Must be done after the above idle rendering, in case it binds a different framebuffer. bind_framebuffer(); } @@ -1608,31 +1799,9 @@ void Canvas::paint_widget(const Cairo::RefPtr &cr) args.splitdir = _split_direction; args.hoverdir = _hover_direction; args.yaxisdir = _desktop ? _desktop->yaxisdir() : 1.0; - args.clean_region = d->updater->clean_region; d->graphics->paint_widget(Fragment{ _affine, get_area_world() }, args, cr); - // Process bucketed events as soon as possible after draw. We cannot process them now, because we have - // a frame to get out as soon as possible, and processing events may take a while. Instead, we schedule - // it with a signal callback on the main loop that runs as soon as this function is completed. - if (!d->eventprocessor->events.empty()) d->schedule_bucket_emptier(); - - // Record the fact that a draw is no longer pending. - d->pending_draw = false; - - // Notify the update strategy that another frame has passed. - d->updater->next_frame(); - - // If asked, print idle time utilisation stats. - if (d->prefs.debug_idle_starvation && d->sample_begin != 0) { - auto elapsed = g_get_monotonic_time() - d->sample_begin; - auto overhead = 100 * d->wait_accumulated / elapsed; - auto col = overhead < 5 ? "\033[1;32m" : overhead < 20 ? "\033[1;33m" : "\033[1;31m"; - std::cout << "Overhead: " << col << overhead << "%" << "\033[0m" << (d->idle_running ? " [still busy]" : "") << std::endl; - d->sample_begin = d->wait_begin = g_get_monotonic_time(); - d->wait_accumulated = 0; - } - // If asked, run an animation loop. if (d->prefs.debug_animate) { auto t = g_get_monotonic_time() / 1700000.0; @@ -1643,34 +1812,9 @@ void Canvas::paint_widget(const Cairo::RefPtr &cr) } } -void CanvasPrivate::add_idle() -{ - framecheck_whole_function(this) - - if (!active) { - // We can safely discard events until active, because we will run add_idle on activation later in initialisation. - return; - } - - if (prefs.debug_idle_starvation && !idle_running) { - auto time = g_get_monotonic_time(); - if (sample_begin == 0) { - sample_begin = time; - wait_accumulated = 0; - } - wait_begin = time; - } - - if (!hipri_idle.connected()) { - hipri_idle = Glib::signal_idle().connect(sigc::mem_fun(*this, &CanvasPrivate::on_hipri_idle), G_PRIORITY_HIGH_IDLE + 15); // after resize, before draw - } - - if (!lopri_idle.connected()) { - lopri_idle = Glib::signal_idle().connect(sigc::mem_fun(*this, &CanvasPrivate::on_lopri_idle), G_PRIORITY_DEFAULT_IDLE); - } - - idle_running = true; -} +/* + * Async redrawing process + */ // Replace a region with a larger region consisting of fewer, larger rectangles. (Allowed to slightly overlap.) auto coarsen(const Cairo::RefPtr ®ion, int min_size, int glue_size, double min_fullness) @@ -1775,57 +1919,18 @@ auto coarsen(const Cairo::RefPtr ®ion, int min_size, int glue_ return processed; } -std::optional CanvasPrivate::old_bisector(const Geom::IntRect &rect) -{ - int bw = rect.width(); - int bh = rect.height(); - - /* - * Determine redraw strategy: - * - * bw < bh (strips mode): Draw horizontal strips starting from cursor position. - * Seems to be faster for drawing many smaller objects zoomed out. - * - * bw > hb (chunks mode): Splits across the larger dimension of the rectangle, painting - * in almost square chunks (from the cursor. - * Seems to be faster for drawing a few blurred objects across the entire screen. - * Seems to be somewhat psychologically faster. - * - * Default is for strips mode. - */ - - int max_pixels; - if (q->_render_mode != Inkscape::RenderMode::OUTLINE) { - // Can't be too small or large gradient will be rerendered too many times! - max_pixels = 65536 * prefs.tile_multiplier; - } else { - // Paths only. 1M is catched buffer and we need four channels. - max_pixels = 262144; - } - - if (bw * bh > max_pixels) { - if (bw < bh || bh < 2 * prefs.tile_size) { - return Geom::X; - } else { - return Geom::Y; - } - } - - return {}; -} - -std::optional CanvasPrivate::new_bisector(const Geom::IntRect &rect) +static std::optional bisect(Geom::IntRect const &rect, int tile_size) { int bw = rect.width(); int bh = rect.height(); // Chop in half along the bigger dimension if the bigger dimension is too big. if (bw > bh) { - if (bw > prefs.new_bisector_size) { + if (bw > tile_size) { return Geom::X; } } else { - if (bh > prefs.new_bisector_size) { + if (bh > tile_size) { return Geom::Y; } } @@ -1833,323 +1938,289 @@ std::optional CanvasPrivate::new_bisector(const Geom::IntRect &rect) return {}; } -bool CanvasPrivate::on_hipri_idle() +void CanvasPrivate::init_tiler() { - on_lopri_idle(); - return false; -} + // Begin processing redraws. + rd.start_time = g_get_monotonic_time(); + rd.phase = 0; + rd.vis_store = regularised(rd.visible & rd.store.rect); -bool CanvasPrivate::on_lopri_idle() -{ - assert(active); - if (idle_running) { - if (prefs.debug_idle_starvation) wait_accumulated += g_get_monotonic_time() - wait_begin; - idle_running = on_idle(); - if (prefs.debug_idle_starvation && idle_running) wait_begin = g_get_monotonic_time(); + if (!init_redraw()) { + sync.signalExit(); + return; } - return idle_running; -} - -void CanvasPrivate::handle_stores_action(Stores::Action action) -{ - switch (action) { - case Stores::Action::Recreated: - // Set everything as needing redraw. - updater->reset(); - - if (prefs.debug_show_unclean) q->queue_draw(); - break; - case Stores::Action::Shifted: - updater->intersect(stores.store().rect); + // Launch render threads to process tiles. + rd.timeoutflag = false; - if (prefs.debug_show_unclean) q->queue_draw(); - break; - - default: - break; - } + #pragma omp parallel for num_threads(rd.numthreads) + for (int i = 0; i < rd.numthreads; i++) { + render_tile(i); + } - if (action != Stores::Action::None) { - q->_drawing->setCacheLimit(stores.store().rect); - } + rd.rects.clear(); + sync.signalExit(); } -bool CanvasPrivate::on_idle() +bool CanvasPrivate::init_redraw() { - framecheck_whole_function(this) - - assert(active); // Guaranteed since already checked by both callers. - assert(canvasitem_ctx->root()); - - // Quit idle process if not supposed to be drawing. - if (q->_drawing_disabled) { - return false; - } - - // Because GTK keeps making it not current. - if (q->get_opengl_enabled()) q->make_current(); + assert(rd.rects.empty()); - if ((outlines_required() && !outlines_enabled) || scale_factor != q->get_scale_factor()) { - stores.reset(); - } - - outlines_enabled = outlines_required(); - scale_factor = q->get_scale_factor(); - - pi.pages.clear(); - canvasitem_ctx->root()->visit_page_rects([this] (auto &rect) { - pi.pages.emplace_back(rect); - }); + switch (rd.phase) { + case 0: + if (rd.vis_store && rd.decoupled_mode) { + // The highest priority to redraw is the region that is visible but not covered by either clean or snapshot content, if in decoupled mode. + // If this is not rendered immediately, it will be perceived as edge flicker, most noticeably on zooming out, but also on rotation too. + process_redraw(*rd.vis_store, unioned(updater->clean_region->copy(), rd.snapshot_drawn)); + return true; + } else { + rd.phase++; + // fallthrough + } - graphics->set_outlines_enabled(outlines_enabled); - graphics->set_scale_factor(scale_factor); - graphics->set_colours(page, desk, border); - graphics->set_background_in_stores(require_background_in_stores()); + case 1: + // Another high priority to redraw is the grabbed canvas item, if the user has requested block updates. + if (rd.grabbed) { + process_redraw(*rd.grabbed, updater->clean_region, false, false); // non-interruptible, non-preemptible + return true; + } else { + rd.phase++; + // fallthrough + } - auto ret = stores.update(Fragment{ q->_affine, q->get_area_world() }); - handle_stores_action(ret); + case 2: + if (rd.vis_store) { + // The main priority to redraw, and the bread and butter of Inkscape's painting, is the visible content that is not clean. + // This may be done over several cycles, at the direction of the Updater, each outwards from the mouse. + process_redraw(*rd.vis_store, updater->get_next_clean_region()); + return true; + } else { + rd.phase++; + // fallthrough + } - if (clip_to_page) { - Geom::PathVector pv; - for (auto &rect : pi.pages) { - pv.push_back(Geom::Path(rect)); + case 3: { + // The lowest priority to redraw is the prerender margin around the visible rectangle. + // (This is in addition to any opportunistic prerendering that may have already occurred in the above steps.) + auto prerender = expandedBy(rd.visible, rd.margin); + auto prerender_store = regularised(prerender & rd.store.rect); + if (prerender_store) { + // Before starting, we request that the tiles drawn up to this point are flushed so they don't have to wait + // for prerendering to finish. (Note: Some yet-to-be-drawn tiles may be committed too; this is harmless.) + commit_tiles_dispatcher.emit(); + process_redraw(*prerender_store, updater->clean_region); + return true; + } else { + return false; + } } - q->_drawing->setClip(std::move(pv)); - } else { - q->_drawing->setClip({}); - } - - // Assert that the clean region is a subregion of the store. - #ifndef NDEBUG - auto tmp = updater->clean_region->copy(); - tmp->subtract(geom_to_cairo(stores.store().rect)); - assert(tmp->empty()); - #endif - // Ensure the geometry is up-to-date and in the right place. - auto const &affine = stores.store().affine; - bool const affine_changed = canvasitem_ctx->affine() != stores.store().affine; - if (q->_need_update || affine_changed) { - q->_need_update = false; - canvasitem_ctx->setAffine(affine); - canvasitem_ctx->root()->update(affine_changed); + default: + assert(false); + return false; } +} - // If asked to, don't paint anything and instead halt the idle process. - if (prefs.debug_disable_redraw) { - return false; - } +// Paint a given subrectangle of the store given by 'bounds', but avoid painting the part of it within 'clean' if possible. +// Some parts both outside the bounds and inside the clean region may also be painted if it helps reduce fragmentation. +void CanvasPrivate::process_redraw(Geom::IntRect const &bounds, Cairo::RefPtr clean, bool interruptible, bool preemptible) +{ + rd.bounds = bounds; + rd.clean = std::move(clean); + rd.interruptible = interruptible; + rd.preemptible = preemptible; - // Get the mouse position in screen space. - Geom::IntPoint mouse_loc = (last_mouse ? *last_mouse : Geom::Point(q->get_dimensions()) / 2).round(); + // Assert that we do not render outside of store. + assert(rd.store.rect.contains(rd.bounds)); - // Map the mouse to canvas space. - mouse_loc += q->_pos; - if (stores.mode() == Stores::Mode::Decoupled) { - mouse_loc = (Geom::Point(mouse_loc) * q->_affine.inverse() * stores.store().affine).round(); - } + // Get the region we are asked to paint. + auto region = Cairo::Region::create(geom_to_cairo(rd.bounds)); + region->subtract(rd.clean); - // Get the visible rect. - Geom::IntRect visible = q->get_area_world(); - if (stores.mode() == Stores::Mode::Decoupled) { - visible = (Geom::Parallelogram(visible) * q->_affine.inverse() * stores.store().affine).bounds().roundOutwards(); - } + // Get the list of rectangles to paint, coarsened to avoid fragmentation. + rd.rects = coarsen(region, + std::min(rd.coarsener_min_size, rd.tile_size / 2), + std::min(rd.coarsener_glue_size, rd.tile_size / 2), + rd.coarsener_min_fullness); - // Begin processing redraws. - auto start_time = g_get_monotonic_time(); - - // Paint a given subrectangle of the store given by 'bounds', but avoid painting the part of it within 'clean' if possible. - // Some parts both outside the bounds and inside the clean region may also be painted if it helps reduce fragmentation. - // Returns true to indicate timeout. - auto process_redraw = [&, this] (Geom::IntRect const &bounds, Cairo::RefPtr const &clean, bool interruptible = true, bool preemptible = true) { - // Assert that we do not render outside of store. - assert(stores.store().rect.contains(bounds)); - - // Get the region we are asked to paint. - auto region = Cairo::Region::create(geom_to_cairo(bounds)); - region->subtract(clean); - - // Get the list of rectangles to paint, coarsened to avoid fragmentation. - auto rects = coarsen(region, - std::min(prefs.coarsener_min_size, prefs.new_bisector_size / 2), - std::min(prefs.coarsener_glue_size, prefs.new_bisector_size / 2), - prefs.coarsener_min_fullness); - - // Put the rectangles into a heap sorted by distance from mouse. - auto cmp = [&] (const Geom::IntRect &a, const Geom::IntRect &b) { - return distSq(mouse_loc, a) > distSq(mouse_loc, b); - }; - std::make_heap(rects.begin(), rects.end(), cmp); + // Put the rectangles into a heap sorted by distance from mouse. + std::make_heap(rd.rects.begin(), rd.rects.end(), rd.getcmp()); +} - // Process rectangles until none left or timed out. - while (!rects.empty()) { - // Extract the closest rectangle to the mouse. - std::pop_heap(rects.begin(), rects.end(), cmp); - auto rect = rects.back(); - rects.pop_back(); +// Process rectangles until none left or timed out. +void CanvasPrivate::render_tile(int debug_id) +{ + rd.mutex.lock(); - // Cull empty rectangles. - if (rect.hasZeroArea()) { - continue; - } + std::string fc_str; + FrameCheck::Event fc; + if (rd.debug_framecheck) { + fc_str = "render_thread_" + std::to_string(debug_id + 1); + fc = FrameCheck::Event(fc_str.c_str()); + } - // Cull rectangles that lie entirely inside the clean region. - // (These can be generated by coarsening; they must be discarded to avoid getting stuck re-rendering the same rectangles.) - if (clean->contains_rectangle(geom_to_cairo(rect)) == Cairo::REGION_OVERLAP_IN) { + while (true) { + // If we've run out of rects, try to start a new redraw cycle. + if (rd.rects.empty()) { + if (end_redraw()) { + // More redraw cycles to do. continue; + } else { + // All finished. + break; } + } - // Lambda to add a rectangle to the heap. - auto add_rect = [&] (const Geom::IntRect &rect) { - rects.emplace_back(rect); - std::push_heap(rects.begin(), rects.end(), cmp); - }; - - // If the rectangle needs bisecting, bisect it and put it back on the heap. - // Note: Currently we disable bisection if interruptible is false, because the only point of bisection is - // to stay within the timeout. However in the future, with tile parallelisation, this will no longer hold. - auto axis = prefs.use_new_bisector ? new_bisector(rect) : old_bisector(rect); - if (axis && interruptible) { - int mid = rect[*axis].middle(); - auto lo = rect; lo[*axis].setMax(mid); add_rect(lo); - auto hi = rect; hi[*axis].setMin(mid); add_rect(hi); - continue; - } + // Check for cancellation. + auto const flags = abort_flags.load(std::memory_order_relaxed); + bool const soft = flags & (int)AbortFlags::Soft; + bool const hard = flags & (int)AbortFlags::Hard; + if (hard || (rd.phase == 3 && soft)) { + break; + } - // Extend thin rectangles at the edge of the bounds rect to at least some minimum size, being sure to keep them within the store. - // (This ensures we don't end up rendering one thin rectangle at the edge every frame while the view is moved continuously.) - if (preemptible) { - if (rect.width() < prefs.preempt) { - if (rect.left() == bounds.left() ) rect.setLeft (std::max(rect.right() - prefs.preempt, stores.store().rect.left() )); - if (rect.right() == bounds.right()) rect.setRight(std::min(rect.left() + prefs.preempt, stores.store().rect.right())); - } - if (rect.height() < prefs.preempt) { - if (rect.top() == bounds.top() ) rect.setTop (std::max(rect.bottom() - prefs.preempt, stores.store().rect.top() )); - if (rect.bottom() == bounds.bottom()) rect.setBottom(std::min(rect.top() + prefs.preempt, stores.store().rect.bottom())); - } - } + // Extract the closest rectangle to the mouse. + std::pop_heap(rd.rects.begin(), rd.rects.end(), rd.getcmp()); + auto rect = rd.rects.back(); + rd.rects.pop_back(); - // Paint the rectangle. - paint_rect(rect); + // Cull empty rectangles. + if (rect.hasZeroArea()) { + continue; + } - // Introduce an artificial delay for each rectangle. - if (prefs.debug_slow_redraw) g_usleep(prefs.debug_slow_redraw_time); + // Cull rectangles that lie entirely inside the clean region. + // (These can be generated by coarsening; they must be discarded to avoid getting stuck re-rendering the same rectangles.) + if (rd.clean->contains_rectangle(geom_to_cairo(rect)) == Cairo::REGION_OVERLAP_IN) { + continue; + } - // Mark the rectangle as clean. - updater->mark_clean(rect); - stores.mark_drawn(rect); + // Lambda to add a rectangle to the heap. + auto add_rect = [&] (Geom::IntRect const &rect) { + rd.rects.emplace_back(rect); + std::push_heap(rd.rects.begin(), rd.rects.end(), rd.getcmp()); + }; - // Get the rectangle of screen-space needing repaint. - Geom::IntRect repaint_rect; - if (stores.mode() != Stores::Mode::Decoupled) { - // Simply translate to get back to screen space. - repaint_rect = rect - q->_pos; - } else { - // Transform into screen space, take bounding box, and round outwards. - auto pl = Geom::Parallelogram(rect); - pl *= stores.store().affine.inverse() * q->_affine; - pl *= Geom::Translate(-q->_pos); - repaint_rect = pl.bounds().roundOutwards(); - } + // If the rectangle needs bisecting, bisect it and put it back on the heap. + // Note: In single-thread mode, bisection should be disabled if interruptible is set. + if (auto axis = bisect(rect, rd.tile_size)) { + int mid = rect[*axis].middle(); + auto lo = rect; lo[*axis].setMax(mid); add_rect(lo); + auto hi = rect; hi[*axis].setMin(mid); add_rect(hi); + continue; + } - // Check if repaint is necessary - some rectangles could be entirely off-screen. - auto screen_rect = Geom::IntRect({0, 0}, q->get_dimensions()); - if (regularised(repaint_rect & screen_rect)) { - // Schedule repaint. - queue_draw_area(repaint_rect); - disconnect_bucket_emptier_tick_callback(); - pending_draw = true; + // Extend thin rectangles at the edge of the bounds rect to at least some minimum size, being sure to keep them within the store. + // (This ensures we don't end up rendering one thin rectangle at the edge every frame while the view is moved continuously.) + if (rd.preemptible) { + if (rect.width() < rd.preempt) { + if (rect.left() == rd.bounds.left() ) rect.setLeft (std::max(rect.right() - rd.preempt, rd.store.rect.left() )); + if (rect.right() == rd.bounds.right()) rect.setRight(std::min(rect.left() + rd.preempt, rd.store.rect.right())); } - - // Check for timeout. - if (interruptible) { - auto now = g_get_monotonic_time(); - auto elapsed = now - start_time; - if (elapsed > prefs.render_time_limit) { - // Timed out. Temporarily return to GTK main loop, and come back here when next idle. - if (prefs.debug_logging) std::cout << "Timed out: " << elapsed << " us" << std::endl; - framecheckobj.subtype = 1; - return true; - } + if (rect.height() < rd.preempt) { + if (rect.top() == rd.bounds.top() ) rect.setTop (std::max(rect.bottom() - rd.preempt, rd.store.rect.top() )); + if (rect.bottom() == rd.bounds.bottom()) rect.setBottom(std::min(rect.top() + rd.preempt, rd.store.rect.bottom())); } } - // No timeout. - return false; - }; + // Mark the rectangle as clean. + updater->mark_clean(rect); - if (auto vis_store = regularised(visible & stores.store().rect)) { - // The highest priority to redraw is the region that is visible but not covered by either clean or snapshot content, if in decoupled mode. - // If this is not rendered immediately, it will be perceived as edge flicker, most noticeably on zooming out, but also on rotation too. - if (stores.mode() == Stores::Mode::Decoupled) { - if (process_redraw(*vis_store, unioned(updater->clean_region->copy(), stores.snapshot().drawn))) return true; - } + rd.mutex.unlock(); - // Another high priority to redraw is the grabbed canvas item, if the user has requested block updates. - if (q->_grabbed_canvas_item && prefs.block_updates) { - if (auto grabbed = regularised(roundedOutwards(q->_grabbed_canvas_item->get_bounds()) & *vis_store)) { - process_redraw(*grabbed, updater->clean_region, false, false); // non-interruptible, non-preemptible - // Reset timeout to leave the normal amount of time for clearing up artifacts. - start_time = g_get_monotonic_time(); - } - } + // Paint the rectangle. + paint_rect(rect); + + rd.mutex.lock(); - // The main priority to redraw, and the bread and butter of Inkscape's painting, is the visible content that is not clean. - // This may be done over several cycles, at the direction of the Updater, each outwards from the mouse. - do { - if (process_redraw(*vis_store, updater->get_next_clean_region())) return true; + // Check for timeout. + if (rd.interruptible) { + auto now = g_get_monotonic_time(); + auto elapsed = now - rd.start_time; + if (elapsed > rd.render_time_limit * 1000) { + // Timed out. Temporarily return to GTK main loop, and come back here when next idle. + rd.timeoutflag = true; + break; + } } - while (updater->report_finished()); } - // The lowest priority to redraw is the prerender margin around the visible rectangle. - // (This is in addition to any opportunistic prerendering that may have already occurred in the above steps.) - auto prerender = expandedBy(visible, prefs.prerender); - auto prerender_store = regularised(prerender & stores.store().rect); - if (prerender_store) { - if (process_redraw(*prerender_store, updater->clean_region)) return true; + if (rd.debug_framecheck && rd.timeoutflag) { + fc.subtype = 1; } - // Finished drawing. Handle transitions out of decoupled mode, by checking if we need to do a final redraw at the correct affine. - ret = stores.finished_draw(Fragment{ q->_affine, q->get_area_world() }); - handle_stores_action(ret); + rd.mutex.unlock(); +} - if (ret != Stores::Action::None) { - // Continue idle process. - return true; - } else { - // All done, quit the idle process. - framecheckobj.subtype = 3; - return false; +bool CanvasPrivate::end_redraw() +{ + switch (rd.phase) { + case 0: + rd.phase++; + return init_redraw(); + + case 1: + rd.phase++; + // Reset timeout to leave the normal amount of time for clearing up artifacts. + rd.start_time = g_get_monotonic_time(); + return init_redraw(); + + case 2: + if (!updater->report_finished()) { + rd.phase++; + } + return init_redraw(); + + case 3: + return false; + + default: + assert(false); + return false; } } -void CanvasPrivate::paint_rect(const Geom::IntRect &rect) +void CanvasPrivate::paint_rect(Geom::IntRect const &rect) { // Make sure the paint rectangle lies within the store. - assert(stores.store().rect.contains(rect)); + assert(rd.store.rect.contains(rect)); auto paint = [&, this] (bool need_background, bool outline_pass) { - auto surface = graphics->request_tile_surface(rect, outline_pass); + + auto surface = graphics->request_tile_surface(rect, true); + if (!surface) { + sync.runInMain([&] { + if (prefs.debug_logging) std::cout << "Blocked - buffer mapping" << std::endl; + if (q->get_opengl_enabled()) q->make_current(); + surface = graphics->request_tile_surface(rect, false); + }); + } + paint_single_buffer(surface, rect, need_background, outline_pass); + return surface; }; - Fragment fragment; - fragment.affine = stores.store().affine; - fragment.rect = rect; - - Cairo::RefPtr surface, outline_surface; - surface = paint(require_background_in_stores(), false); + // Create and render the tile. + Tile tile; + tile.fragment.affine = rd.store.affine; + tile.fragment.rect = rect; + tile.surface = paint(background_in_stores_required(), false); if (outlines_enabled) { - outline_surface = paint(false, true); + tile.outline_surface = paint(false, true); } - graphics->draw_tile(fragment, surface, outline_surface); + // Introduce an artificial delay for each rectangle. + if (rd.redraw_delay) g_usleep(*rd.redraw_delay); + + // Stick the tile on the list of tiles to reap. + { + auto g = std::lock_guard(rd.tiles_mutex); + rd.tiles.emplace_back(std::move(tile)); + } } -void CanvasPrivate::paint_single_buffer(const Cairo::RefPtr &surface, const Geom::IntRect &rect, bool need_background, bool outline_pass) +void CanvasPrivate::paint_single_buffer(Cairo::RefPtr const &surface, Geom::IntRect const &rect, bool need_background, bool outline_pass) { // Create Cairo context. auto cr = Cairo::Context::create(surface); @@ -2157,7 +2228,7 @@ void CanvasPrivate::paint_single_buffer(const Cairo::RefPtr // Clear background. cr->save(); if (need_background) { - Graphics::paint_background(Fragment{ stores.store().affine, rect }, pi, page, desk, cr); + Graphics::paint_background(Fragment{ rd.store.affine, rect }, pi, rd.page, rd.desk, cr); } else { cr->set_operator(Cairo::OPERATOR_CLEAR); cr->paint(); @@ -2169,32 +2240,14 @@ void CanvasPrivate::paint_single_buffer(const Cairo::RefPtr canvasitem_ctx->root()->render(buf); // Paint over newly drawn content with a translucent random colour. - if (prefs.debug_show_redraw) { + if (rd.debug_show_redraw) { cr->set_source_rgba((rand() % 256) / 255.0, (rand() % 256) / 255.0, (rand() % 256) / 255.0, 0.2); cr->set_operator(Cairo::OPERATOR_OVER); cr->paint(); } - - if (q->_cms_active) { - auto transf = prefs.from_display - ? Inkscape::CMSSystem::getDisplayPer(q->_cms_key) - : Inkscape::CMSSystem::getDisplayTransform(); - if (transf) { - surface->flush(); - auto px = surface->get_data(); - int stride = surface->get_stride(); - for (int i = 0; i < rect.height(); i++) { - auto row = px + i * stride; - Inkscape::CMSSystem::doTransform(transf, row, row, rect.width()); - } - surface->mark_dirty(); - } - } } -} // namespace Widget -} // namespace UI -} // namespace Inkscape +} // namespace Inkscape::UI::Widget /* Local Variables: diff --git a/src/ui/widget/canvas.h b/src/ui/widget/canvas.h index 24ef9a98dc..b483145cdd 100644 --- a/src/ui/widget/canvas.h +++ b/src/ui/widget/canvas.h @@ -106,19 +106,17 @@ public: // Invalidation void redraw_all(); // Mark everything as having changed. - void redraw_area(Geom::Rect &area); // Mark a rectangle of world space as having changed. + void redraw_area(Geom::Rect const &area); // Mark a rectangle of world space as having changed. void redraw_area(int x0, int y0, int x1, int y1); void redraw_area(Geom::Coord x0, Geom::Coord y0, Geom::Coord x1, Geom::Coord y1); void request_update(); // Mark geometry as needing recalculation. - // Event compression and gobbling (tool-base.cpp) - void set_event_compression(bool enabled); - int gobble_key_events(guint keyval, guint mask); - void gobble_motion_events(guint mask); - // Callback run on destructor of any canvas item void canvas_item_destructed(Inkscape::CanvasItem *item); + // Wait for background process to become inactive. (Todo: Remove this non-reentrancy workaround when possible.) + void wait_for_drawing_inactive() const; + // State Inkscape::CanvasItem *get_current_canvas_item() const { return _current_canvas_item; } void set_current_canvas_item(Inkscape::CanvasItem *item) { @@ -201,7 +199,7 @@ private: Geom::Point _split_frac; Inkscape::SplitDirection _hover_direction; bool _split_dragging; - Geom::Point _split_drag_start; + Geom::IntPoint _split_drag_start; void set_cursor(); diff --git a/src/ui/widget/canvas/cairographics.cpp b/src/ui/widget/canvas/cairographics.cpp index f6100c0703..d33a28dfab 100644 --- a/src/ui/widget/canvas/cairographics.cpp +++ b/src/ui/widget/canvas/cairographics.cpp @@ -212,46 +212,31 @@ void CairoGraphics::snapshot_combine(Fragment const &dest) snapshot = std::move(fragment); } -Cairo::RefPtr CairoGraphics::request_tile_surface(Geom::IntRect const &rect, bool outline) +Cairo::RefPtr CairoGraphics::request_tile_surface(Geom::IntRect const &rect, bool /*nogl*/) { - // Create temporary surface that draws directly to store. - auto &src = outline ? store.outline_surface : store.surface; - - src->flush(); - auto data = src->get_data(); - int stride = src->get_stride(); - - // Check we are using the correct device scale. - #ifndef NDEBUG - double x_scale; - double y_scale; - cairo_surface_get_device_scale(src->cobj(), &x_scale, &y_scale); // No C++ API! - assert(x_scale == scale_factor); - assert(y_scale == scale_factor); - #endif - - // Move to the correct row. - data += stride * (rect.top() - stores.store().rect.top()) * scale_factor; - // Move to the correct column. - data += 4 * (rect.left() - stores.store().rect.left()) * scale_factor; - auto surface = Cairo::ImageSurface::create(data, Cairo::FORMAT_ARGB32, - rect.width() * scale_factor, - rect.height() * scale_factor, - stride); - - cairo_surface_set_device_scale(surface->cobj(), scale_factor, scale_factor); // No C++ API! - + // 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 surface, Cairo::RefPtr outline_surface) { - surface.clear(); - store.surface->mark_dirty(); + // 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) { - outline_surface.clear(); - store.outline_surface->mark_dirty(); + 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(); } } @@ -403,7 +388,7 @@ void CairoGraphics::paint_widget(Fragment const &view, PaintArgs const &a, Cairo 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.x_ray_radius, 0, 2 * M_PI); + 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. @@ -417,37 +402,6 @@ void CairoGraphics::paint_widget(Fragment const &view, PaintArgs const &a, Cairo // The rest can be done with antialiasing. cr->set_antialias(Cairo::ANTIALIAS_DEFAULT); - // Paint unclean regions in red. - if (prefs.debug_show_unclean) { - if (prefs.debug_framecheck) f = FrameCheck::Event("paint_unclean"); - cr->set_operator(Cairo::OPERATOR_OVER); - auto reg = Cairo::Region::create(geom_to_cairo(stores.store().rect)); - reg->subtract(a.clean_region); - cr->save(); - cr->translate(-view.rect.left(), -view.rect.top()); - if (stores.mode() == Stores::Mode::Decoupled) { - cr->transform(geom_to_cairo(stores.store().affine.inverse() * view.affine)); - } - cr->set_source_rgba(1, 0, 0, 0.2); - region_to_path(cr, reg); - cr->fill(); - cr->restore(); - } - - // Paint internal edges of clean region in green. - if (prefs.debug_show_clean) { - if (prefs.debug_framecheck) f = FrameCheck::Event("paint_clean"); - cr->save(); - cr->translate(-view.rect.left(), -view.rect.top()); - if (stores.mode() == Stores::Mode::Decoupled) { - cr->transform(geom_to_cairo(stores.store().affine.inverse() * view.affine)); - } - cr->set_source_rgba(0, 0.7, 0, 0.4); - region_to_path(cr, a.clean_region); - cr->stroke(); - cr->restore(); - } - if (a.splitmode == Inkscape::SplitMode::SPLIT) { paint_splitview_controller(view.rect.dimensions(), a.splitfrac, a.splitdir, a.hoverdir, cr); } diff --git a/src/ui/widget/canvas/cairographics.h b/src/ui/widget/canvas/cairographics.h index c066074a2a..c5d949bdde 100644 --- a/src/ui/widget/canvas/cairographics.h +++ b/src/ui/widget/canvas/cairographics.h @@ -40,7 +40,7 @@ public: bool is_opengl() const override { return false; } void invalidated_glstate() override {} - Cairo::RefPtr request_tile_surface(Geom::IntRect const &rect, bool outline) override; + Cairo::RefPtr request_tile_surface(Geom::IntRect const &rect, bool nogl) override; void draw_tile(Fragment const &fragment, Cairo::RefPtr surface, Cairo::RefPtr outline_surface) override; void paint_widget(Fragment const &view, PaintArgs const &args, Cairo::RefPtr const &cr) override; diff --git a/src/ui/widget/canvas/fragment.h b/src/ui/widget/canvas/fragment.h index f015a35f39..d3edc74890 100644 --- a/src/ui/widget/canvas/fragment.h +++ b/src/ui/widget/canvas/fragment.h @@ -5,9 +5,7 @@ #include <2geom/int-rect.h> #include <2geom/affine.h> -namespace Inkscape { -namespace UI { -namespace Widget { +namespace Inkscape::UI::Widget { /// A "fragment" is a rectangle of drawn content at a specfic place. struct Fragment @@ -19,9 +17,7 @@ struct Fragment Geom::IntRect rect; }; -} // namespace Widget -} // namespace UI -} // namespace Inkscape +} // namespace Inkscape::UI::Widget #endif // INKSCAPE_UI_WIDGET_CANVAS_FRAGMENT_H diff --git a/src/ui/widget/canvas/framecheck.cpp b/src/ui/widget/canvas/framecheck.cpp index 27b3d5bf36..c127c8e805 100644 --- a/src/ui/widget/canvas/framecheck.cpp +++ b/src/ui/widget/canvas/framecheck.cpp @@ -1,29 +1,24 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include #include +#include #include // Using boost::filesystem instead of std::filesystem due to broken C++17 on MacOS. #include "framecheck.h" namespace fs = boost::filesystem; -namespace Inkscape { -namespace FrameCheck { +namespace Inkscape::FrameCheck { -std::ostream &logfile() +void Event::write() { - static std::ofstream f; - - if (!f.is_open()) { - try { - auto path = fs::temp_directory_path() / "framecheck.txt"; - auto mode = std::ios_base::out | std::ios_base::app | std::ios_base::binary; - f.open(path.string(), mode); - } catch (...) { - std::cerr << "failed to create framecheck logfile" << std::endl; - } - } - - return f; + 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 FrameCheck -} // namespace Inkscape +} // namespace Inkscape::FrameCheck diff --git a/src/ui/widget/canvas/framecheck.h b/src/ui/widget/canvas/framecheck.h index cbf6eaba13..8964561e1d 100644 --- a/src/ui/widget/canvas/framecheck.h +++ b/src/ui/widget/canvas/framecheck.h @@ -2,13 +2,9 @@ #ifndef INKSCAPE_FRAMECHECK_H #define INKSCAPE_FRAMECHECK_H -#include #include -namespace Inkscape { -namespace FrameCheck { - -extern std::ostream &logfile(); +namespace Inkscape::FrameCheck { /// RAII object that logs a timing event for the duration of its lifetime. struct Event @@ -41,16 +37,12 @@ private: p.start = -1; } - void finish() - { - if (start != -1) { - logfile() << name << ' ' << start << ' ' << g_get_monotonic_time() << ' ' << subtype << std::endl; - } - } + void finish() { if (start != -1) write(); } + + void write(); }; -} // namespace FrameCheck -} // namespace Inkscape +} // namespace Inkscape::FrameCheck #endif // INKSCAPE_FRAMECHECK_H diff --git a/src/ui/widget/canvas/glgraphics.cpp b/src/ui/widget/canvas/glgraphics.cpp index 26f8b24e2c..b47092b331 100644 --- a/src/ui/widget/canvas/glgraphics.cpp +++ b/src/ui/widget/canvas/glgraphics.cpp @@ -595,9 +595,14 @@ void GLGraphics::setup_tiles_pipeline() glDisable(GL_BLEND); }; -Cairo::RefPtr GLGraphics::request_tile_surface(Geom::IntRect const &rect, bool /*outline*/) +Cairo::RefPtr GLGraphics::request_tile_surface(Geom::IntRect const &rect, bool nogl) { - auto surface = pixelstreamer->request(rect.dimensions() * scale_factor); + Cairo::RefPtr 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); @@ -608,6 +613,7 @@ Cairo::RefPtr GLGraphics::request_tile_surface(Geom::IntRec void GLGraphics::draw_tile(Fragment const &fragment, Cairo::RefPtr surface, Cairo::RefPtr outline_surface) { + auto g = std::lock_guard(ps_mutex); auto surface_size = dimensions(surface); Texture texture, outline_texture; @@ -839,7 +845,7 @@ void GLGraphics::paint_widget(Fragment const &view, PaintArgs const &a, Cairo::R // 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.x_ray_radius * scale_factor); + 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); } diff --git a/src/ui/widget/canvas/glgraphics.h b/src/ui/widget/canvas/glgraphics.h index 0c8def3365..6fb4d1aec7 100644 --- a/src/ui/widget/canvas/glgraphics.h +++ b/src/ui/widget/canvas/glgraphics.h @@ -8,6 +8,7 @@ #ifndef INKSCAPE_UI_WIDGET_CANVAS_GLGRAPHICS_H #define INKSCAPE_UI_WIDGET_CANVAS_GLGRAPHICS_H +#include #include #include "graphics.h" #include "texturecache.h" @@ -83,7 +84,7 @@ public: bool is_opengl() const override { return true; } void invalidated_glstate() override { state = State::None; } - Cairo::RefPtr request_tile_surface(Geom::IntRect const &rect, bool outline) override; + Cairo::RefPtr request_tile_surface(Geom::IntRect const &rect, bool nogl) override; void draw_tile(Fragment const &fragment, Cairo::RefPtr surface, Cairo::RefPtr outline_surface) override; void paint_widget(Fragment const &view, PaintArgs const &args, Cairo::RefPtr const &cr) override; @@ -100,6 +101,7 @@ private: // Pixel streamer and texture cache for uploading pixel data to GPU. std::unique_ptr pixelstreamer; std::unique_ptr texturecache; + std::mutex ps_mutex; // For preventing unnecessary pipeline recreation. enum class State { None, Widget, Stores, Tiles }; diff --git a/src/ui/widget/canvas/graphics.h b/src/ui/widget/canvas/graphics.h index fc9a7f3cf2..623dd3035a 100644 --- a/src/ui/widget/canvas/graphics.h +++ b/src/ui/widget/canvas/graphics.h @@ -54,7 +54,7 @@ public: virtual void invalidated_glstate() = 0; ///< Tells the Graphics to no longer rely on any OpenGL state it had set up. // Tile drawing. - virtual Cairo::RefPtr request_tile_surface(Geom::IntRect const &rect, bool outline) = 0; + virtual Cairo::RefPtr request_tile_surface(Geom::IntRect const &rect, bool nogl) = 0; virtual void draw_tile(Fragment const &fragment, Cairo::RefPtr surface, Cairo::RefPtr outline_surface) = 0; // Widget painting. @@ -67,7 +67,6 @@ public: SplitDirection splitdir; SplitDirection hoverdir; double yaxisdir; - Cairo::RefPtr clean_region; // only used for showing debug info }; virtual void paint_widget(Fragment const &view, PaintArgs const &args, Cairo::RefPtr const &cr) = 0; diff --git a/src/ui/widget/canvas/pixelstreamer.cpp b/src/ui/widget/canvas/pixelstreamer.cpp index a06fbe96bd..32f52d07b5 100644 --- a/src/ui/widget/canvas/pixelstreamer.cpp +++ b/src/ui/widget/canvas/pixelstreamer.cpp @@ -9,7 +9,6 @@ namespace Inkscape { namespace UI { namespace Widget { - namespace { cairo_user_data_key_t constexpr key{}; @@ -25,6 +24,7 @@ class PersistentPixelStreamer : public PixelStreamer 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() { @@ -42,6 +42,20 @@ class PersistentPixelStreamer : public PixelStreamer 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 buffers; @@ -58,16 +72,15 @@ class PersistentPixelStreamer : public PixelStreamer std::vector mappings; /* - * A Buffer can be in any one of three states: + * 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 --> Finished the above, but GL may be reading from it. + * 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. - * - * When a Buffer enters the last state, a fence sync object is created. We only recycle the Buffer as the current - * buffer once this sync object has been signalled. When the Buffer leaves this state, the sync object is deleted. */ public: @@ -81,7 +94,7 @@ public: Method get_method() const override { return Method::Persistent; } - Cairo::RefPtr request(Geom::IntPoint const &dimensions) override + Cairo::RefPtr 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()); @@ -89,32 +102,40 @@ public: 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. That means - // creating the sync object for it if necessary. (Handle the transition 1 --> 3.) + // 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].sync = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + buffers[current_buffer].ready = false; + buffers[current_buffer].sync = nogl ? nullptr : glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); } - // Attempt to re-use an old buffer. + // Attempt to re-use a old buffer that has reached state 5. for (int i = 0; i < buffers.size(); i++) { - // Automatically skip the previous current buffer. (In a limbo state at the moment, but will move to 2 or 3 shortly.) - if (i == current_buffer) continue; - // Skip buffers that we are still writing into. (In state 2.) - if (buffers[i].refs > 0) continue; - // Skip buffers that we've finished with, but GL is still reading from. (In state 3, but not ready to leave.) - auto ret = glClientWaitSync(buffers[i].sync, GL_SYNC_FLUSH_COMMANDS_BIT, 0); - if (!(ret == GL_CONDITION_SATISFIED || ret == GL_ALREADY_SIGNALED)) continue; - // Found an unused buffer. Re-use it. (Move to state 1.) - glDeleteSync(buffers[i].sync); - buffers[i].off = 0; - current_buffer = i; - goto chosen_buffer; + 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 {}; } - // Otherwise, there are no available buffers. Create and use a new one. buffers.emplace_back(); buffers.back().create(); current_buffer = buffers.size() - 1; @@ -175,17 +196,25 @@ public: 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 --> 3.) + // 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 3.) + // 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) { + if (i != current_buffer && buffers[i].refs == 0 && !buffers[i].ready && buffers[i].sync) { glDeleteSync(buffers[i].sync); } } @@ -202,22 +231,90 @@ public: class AsynchronousPixelStreamer : public PixelStreamer { - static int constexpr bufsize_multiple = 0x100000; // 1 MiB + static int constexpr minbufsize = 0x4000; // 16 KiB + static int constexpr expire_timeout = 10000; - struct Mapping + 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 { - bool used; GLuint pbo; unsigned char *data; - int size, width, height, stride; + + 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 spares; + int used = 0; + int high_use_count = 0; + }; + std::vector buckets; + + struct Mapping + { + bool used; + Buffer buf; + int bucket; + int width, height, stride; }; std::vector mappings; + int expire_timer = 0; + public: Method get_method() const override { return Method::Asynchronous; } - Cairo::RefPtr request(Geom::IntPoint const &dimensions) override + Cairo::RefPtr 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) { @@ -232,18 +329,13 @@ public: 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 = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, m.width); - m.size = m.stride * m.height; - int bufsize = Util::roundup(m.size, bufsize_multiple); - - glGenBuffers(1, &m.pbo); - glBindBuffer(GL_PIXEL_UNPACK_BUFFER, m.pbo); - glBufferData(GL_PIXEL_UNPACK_BUFFER, bufsize, nullptr, GL_STREAM_DRAW); - m.data = (unsigned char*)glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, m.size, GL_MAP_READ_BIT | GL_MAP_WRITE_BIT); + m.stride = stride; - auto surface = Cairo::ImageSurface::create(m.data, Cairo::FORMAT_ARGB32, m.width, m.height, m.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; } @@ -254,17 +346,55 @@ public: surface.clear(); auto &m = mappings[mapping]; + auto &b = buckets[m.bucket]; - glBindBuffer(GL_PIXEL_UNPACK_BUFFER, m.pbo); + // Unmap the buffer. + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, m.buf.pbo); glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); // Upload the buffer to the texture. 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); - glDeleteBuffers(1, &m.pbo); - + // 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(); + } + } } }; @@ -281,7 +411,7 @@ class SynchronousPixelStreamer : public PixelStreamer public: Method get_method() const override { return Method::Synchronous; } - Cairo::RefPtr request(Geom::IntPoint const &dimensions) override + Cairo::RefPtr request(Geom::IntPoint const &dimensions, bool) override { auto choose_mapping = [&, this] { for (int i = 0; i < mappings.size(); i++) { diff --git a/src/ui/widget/canvas/pixelstreamer.h b/src/ui/widget/canvas/pixelstreamer.h index 3d00c9215d..e87c223ca2 100644 --- a/src/ui/widget/canvas/pixelstreamer.h +++ b/src/ui/widget/canvas/pixelstreamer.h @@ -42,8 +42,11 @@ public: // Return the method in use. virtual Method get_method() const = 0; - // Request a drawing surface of the given dimensions. - virtual Cairo::RefPtr request(Geom::IntPoint const &dimensions) = 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. + */ + virtual Cairo::RefPtr 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. diff --git a/src/ui/widget/canvas/prefs.h b/src/ui/widget/canvas/prefs.h index 22494184ee..86c40fda76 100644 --- a/src/ui/widget/canvas/prefs.h +++ b/src/ui/widget/canvas/prefs.h @@ -4,9 +4,7 @@ #include "preferences.h" -namespace Inkscape { -namespace UI { -namespace Widget { +namespace Inkscape::UI::Widget { class Prefs { @@ -17,46 +15,43 @@ public: devmode.action(); } - // Original parameters - Pref tile_size = { "/options/rendering/tile-size", 16, 1, 10000 }; - Pref tile_multiplier = { "/options/rendering/tile-multiplier", 16, 1, 512 }; - Pref x_ray_radius = { "/options/rendering/xray-radius", 100, 1, 1500 }; - Pref from_display = { "/options/displayprofile/from_display" }; - Pref grabsize = { "/options/grabsize/value", 3, 1, 15 }; + // Main preferences + Pref xray_radius = { "/options/rendering/xray-radius", 100, 1, 1500 }; Pref outline_overlay_opacity = { "/options/rendering/outline-overlay-opacity", 50, 1, 100 }; + Pref update_strategy = { "/options/rendering/update_strategy", 3, 1, 3 }; + Pref request_opengl = { "/options/rendering/request_opengl" }; + Pref grabsize = { "/options/grabsize/value", 3, 1, 15 }; + Pref numthreads = { "/options/threading/numthreads", 0, 1, 256 }; - // Things that require redraws (used by CMS system) - Pref softproof = { "/options/softproof" }; + // Colour management + Pref from_display = { "/options/displayprofile/from_display" }; Pref displayprofile = { "/options/displayprofile" }; + Pref softproof = { "/options/softproof" }; - // New parameters - Pref update_strategy = { "/options/rendering/update_strategy", 3, 1, 3 }; - Pref render_time_limit = { "/options/rendering/render_time_limit", 1000, 100, 1000000 }; - Pref use_new_bisector = { "/options/rendering/use_new_bisector", true }; - Pref new_bisector_size = { "/options/rendering/new_bisector_size", 500, 1, 10000 }; - Pref padding = { "/options/rendering/pad", 350, 0, 1000 }; - Pref prerender = { "/options/rendering/margin", 100, 0, 1000 }; + // Devmode preferences + Pref tile_size = { "/options/rendering/tile_size", 500, 1, 10000 }; + Pref render_time_limit = { "/options/rendering/render_time_limit", 80, 1, 5000 }; + Pref block_updates = { "/options/rendering/block_updates", true }; + Pref pixelstreamer_method = { "/options/rendering/pixelstreamer_method", 1, 1, 4 }; + Pref padding = { "/options/rendering/padding", 350, 0, 1000 }; + Pref prerender = { "/options/rendering/prerender", 100, 0, 1000 }; Pref preempt = { "/options/rendering/preempt", 250, 0, 1000 }; Pref coarsener_min_size = { "/options/rendering/coarsener_min_size", 200, 0, 1000 }; Pref coarsener_glue_size = { "/options/rendering/coarsener_glue_size", 80, 0, 1000 }; Pref coarsener_min_fullness = { "/options/rendering/coarsener_min_fullness", 0.3, 0.0, 1.0 }; - Pref request_opengl = { "/options/rendering/request_opengl" }; - Pref pixelstreamer_method = { "/options/rendering/pixelstreamer_method", 1, 1, 4 }; - Pref block_updates = { "/options/rendering/block_updates", true }; // Debug switches Pref debug_framecheck = { "/options/rendering/debug_framecheck" }; Pref debug_logging = { "/options/rendering/debug_logging" }; - Pref debug_slow_redraw = { "/options/rendering/debug_slow_redraw" }; - Pref debug_slow_redraw_time = { "/options/rendering/debug_slow_redraw_time", 50, 0, 1000000 }; + Pref debug_delay_redraw = { "/options/rendering/debug_delay_redraw" }; + Pref debug_delay_redraw_time = { "/options/rendering/debug_delay_redraw_time", 50, 0, 1000000 }; Pref debug_show_redraw = { "/options/rendering/debug_show_redraw" }; - Pref debug_show_unclean = { "/options/rendering/debug_show_unclean" }; + Pref debug_show_unclean = { "/options/rendering/debug_show_unclean" }; // no longer implemented Pref debug_show_snapshot = { "/options/rendering/debug_show_snapshot" }; - Pref debug_show_clean = { "/options/rendering/debug_show_clean" }; + Pref debug_show_clean = { "/options/rendering/debug_show_clean" }; // no longer implemented Pref debug_disable_redraw = { "/options/rendering/debug_disable_redraw" }; Pref debug_sticky_decoupled = { "/options/rendering/debug_sticky_decoupled" }; Pref debug_animate = { "/options/rendering/debug_animate" }; - Pref debug_idle_starvation = { "/options/rendering/debug_idle_starvation" }; private: // Developer mode @@ -66,19 +61,17 @@ private: { tile_size.set_enabled(on); render_time_limit.set_enabled(on); - use_new_bisector.set_enabled(on); - new_bisector_size.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); - pixelstreamer_method.set_enabled(on); debug_framecheck.set_enabled(on); debug_logging.set_enabled(on); - debug_slow_redraw.set_enabled(on); - debug_slow_redraw_time.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); @@ -86,13 +79,10 @@ private: debug_disable_redraw.set_enabled(on); debug_sticky_decoupled.set_enabled(on); debug_animate.set_enabled(on); - debug_idle_starvation.set_enabled(on); } }; -} // namespace Widget -} // namespace UI -} // namespace Inkscape +} // namespace Inkscape::UI::Widget #endif // INKSCAPE_UI_WIDGET_CANVAS_PREFS_H diff --git a/src/ui/widget/canvas/synchronizer.cpp b/src/ui/widget/canvas/synchronizer.cpp new file mode 100644 index 0000000000..331057b96d --- /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 + +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 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 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(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 &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 0000000000..45c88d2025 --- /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 +#include +#include +#include + +#include +#include + +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 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 const &slot); + +private: + struct Slot + { + std::function const *func; + }; + + Glib::Dispatcher dispatcher; // Used to wake up main thread if idle in GTK main loop. + sigc::signal 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 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 &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/updaters.cpp b/src/ui/widget/canvas/updaters.cpp index 353ae70c37..8441be0eda 100644 --- a/src/ui/widget/canvas/updaters.cpp +++ b/src/ui/widget/canvas/updaters.cpp @@ -11,10 +11,11 @@ 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_clean(Geom::IntRect const &rect) override { clean_region->do_union(geom_to_cairo(rect)); } + 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 const ®) override { clean_region->subtract(reg); } + void mark_clean(Geom::IntRect const &rect) override { clean_region->do_union(geom_to_cairo(rect)); } Cairo::RefPtr get_next_clean_region() override { return clean_region; } bool report_finished () override { return false; } @@ -51,6 +52,12 @@ public: ResponsiveUpdater::mark_dirty(rect); } + void mark_dirty(const Cairo::RefPtr ®) 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); @@ -117,9 +124,20 @@ public: void mark_dirty(Geom::IntRect const &rect) override { ResponsiveUpdater::mark_dirty(rect); + post_mark_dirty(); + } + + void mark_dirty(const Cairo::RefPtr ®) override + { + ResponsiveUpdater::mark_dirty(reg); + post_mark_dirty(); + } + + void post_mark_dirty() + { if (inprogress && !activated) { counter = scale = elapsed = 0; - blocked = {Cairo::Region::create()}; + blocked = { Cairo::Region::create() }; activated = true; } } diff --git a/src/ui/widget/canvas/updaters.h b/src/ui/widget/canvas/updaters.h index fe6175f651..d36685ac1f 100644 --- a/src/ui/widget/canvas/updaters.h +++ b/src/ui/widget/canvas/updaters.h @@ -44,10 +44,11 @@ public: // 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_clean(Geom::IntRect const &) = 0; // Called on every rectangle redrawn. + 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 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 get_next_clean_region() = 0; diff --git a/src/ui/widget/preferences-widget.cpp b/src/ui/widget/preferences-widget.cpp index d918ea769c..f00178eceb 100644 --- a/src/ui/widget/preferences-widget.cpp +++ b/src/ui/widget/preferences-widget.cpp @@ -649,7 +649,7 @@ PrefSlider::init(Glib::ustring const &prefs_path, } void PrefCombo::init(Glib::ustring const &prefs_path, - Glib::ustring labels[], int values[], int num_items, int default_value) + Glib::ustring const labels[], int const values[], int num_items, int default_value) { _prefs_path = prefs_path; Inkscape::Preferences *prefs = Inkscape::Preferences::get(); diff --git a/src/ui/widget/preferences-widget.h b/src/ui/widget/preferences-widget.h index e4935f2793..9a85e1f4aa 100644 --- a/src/ui/widget/preferences-widget.h +++ b/src/ui/widget/preferences-widget.h @@ -186,7 +186,7 @@ class PrefCombo : public Gtk::ComboBoxText { public: void init(Glib::ustring const &prefs_path, - Glib::ustring labels[], int values[], int num_items, int default_value); + Glib::ustring const labels[], int const values[], int num_items, int default_value); /** * Initialize a combo box. diff --git a/src/ui/widget/rotateable.cpp b/src/ui/widget/rotateable.cpp index 7f32823c7e..639f8d1a86 100644 --- a/src/ui/widget/rotateable.cpp +++ b/src/ui/widget/rotateable.cpp @@ -104,6 +104,7 @@ bool Rotateable::on_motion(GdkEventMotion *event) { do_motion(force, modifier); } } + Inkscape::UI::Tools::gobble_motion_events(GDK_BUTTON1_MASK); return true; } return false; -- GitLab