From 26fb4412f0de72d5d425af7487c5316e112b0a08 Mon Sep 17 00:00:00 2001 From: PBS Date: Sun, 9 Jan 2022 11:25:49 +0900 Subject: [PATCH 01/35] Save things while they're working before I break them again. This commit fixes many issues in Canvas. Ctrl+Scroll is now broken, but in an understood, fixable way. --- src/desktop.cpp | 20 +- src/desktop.h | 4 +- src/display/control/canvas-item-ctrl.cpp | 2 +- src/display/control/canvas-item-rect.cpp | 2 +- src/ui/tools/tool-base.cpp | 4 - src/ui/widget/canvas-grid.cpp | 2 +- src/ui/widget/canvas.cpp | 608 ++++++++++------------- src/ui/widget/canvas.h | 77 ++- 8 files changed, 303 insertions(+), 416 deletions(-) diff --git a/src/desktop.cpp b/src/desktop.cpp index 66ad4abcd8..0e3533c2c7 100644 --- a/src/desktop.cpp +++ b/src/desktop.cpp @@ -297,7 +297,6 @@ void SPDesktop::destroy() namedview->hide(this); _sel_changed_connection.disconnect(); - _commit_connection.disconnect(); _reconstruction_start_connection.disconnect(); _reconstruction_finish_connection.disconnect(); @@ -590,9 +589,8 @@ SPDesktop::set_display_area (bool log) // Scroll Geom::Point offset = _current_affine.getOffset(); - canvas->scroll_to(offset, true); + canvas->scroll_to(offset); canvas->set_affine(_current_affine.d2w()); // For CanvasItem's. - // To do: if transform unchanged call with 'false' (redraw only newly exposed areas). /* Update perspective lines if we are in the 3D box tool (so that infinite ones are shown * correctly) */ @@ -660,10 +658,10 @@ SPDesktop::set_display_area( Geom::Rect const &r, double border, bool log) /** * Return canvas viewbox in desktop coordinates */ -Geom::Parallelogram SPDesktop::get_display_area(bool use_integer_viewbox) const +Geom::Parallelogram SPDesktop::get_display_area() const { // viewbox in world coordinates - Geom::Rect const viewbox = use_integer_viewbox ? canvas->get_area_world_int() : canvas->get_area_world(); + Geom::Rect const viewbox = canvas->get_area_world(); // display area in desktop coordinates return Geom::Parallelogram(viewbox) * w2d(); @@ -778,7 +776,7 @@ SPDesktop::zoom_selection() } Geom::Point SPDesktop::current_center() const { - return canvas->get_area_world().midpoint() * _current_affine.w2d(); + return Geom::Point(canvas->get_area_world().midpoint()) * _current_affine.w2d(); } /** @@ -977,7 +975,7 @@ SPDesktop::is_flipped (CanvasFlip flip) void SPDesktop::scroll_absolute (Geom::Point const &point, bool is_scrolling) { - canvas->scroll_to(point, false); + canvas->scroll_to(point); _current_affine.setOffset( point ); /* update perspective lines if we are in the 3D box tool (so that infinite ones are shown correctly) */ @@ -1295,11 +1293,6 @@ sigc::connection SPDesktop::connectToolSubselectionChangedEx(const sigc::slotredraw_now(); -} - void SPDesktop::updateDialogs() { getContainer()->set_desktop(this); @@ -1420,9 +1413,6 @@ SPDesktop::setDocument (SPDocument *doc) selection->setDocument(doc); - _commit_connection.disconnect(); - _commit_connection = doc->connectCommit(sigc::mem_fun(*this, &SPDesktop::updateNow)); - /// \todo fixme: This condition exists to make sure the code /// inside is NOT called on initialization, only on replacement. But there /// are surely more safe methods to accomplish this. diff --git a/src/desktop.h b/src/desktop.h index aed1980a0b..973e2d6b8f 100644 --- a/src/desktop.h +++ b/src/desktop.h @@ -355,7 +355,7 @@ public: void set_display_area (bool log = true); void set_display_area (Geom::Point const &c, Geom::Point const &w, bool log = true); void set_display_area (Geom::Rect const &a, Geom::Coord border, bool log = true); - Geom::Parallelogram get_display_area(bool use_integer_viewbox = false) const; + Geom::Parallelogram get_display_area() const; void set_display_width(Geom::Rect const &a, Geom::Coord border); void set_display_center(Geom::Rect const &a); @@ -420,7 +420,6 @@ public: Gtk::Widget *get_toolbox() const; void setToolboxAdjustmentValue (gchar const* id, double val); bool isToolboxButtonActive (gchar const *id); - void updateNow(); void updateCanvasNow(); void updateDialogs(); void storeDesktopPosition(); @@ -615,7 +614,6 @@ private: sigc::connection _sel_changed_connection; sigc::connection _reconstruction_start_connection; sigc::connection _reconstruction_finish_connection; - sigc::connection _commit_connection; void onResized (double, double) override; void onRedrawRequested() override; diff --git a/src/display/control/canvas-item-ctrl.cpp b/src/display/control/canvas-item-ctrl.cpp index 11d2cd34e5..437f808cbe 100644 --- a/src/display/control/canvas-item-ctrl.cpp +++ b/src/display/control/canvas-item-ctrl.cpp @@ -320,7 +320,7 @@ void CanvasItemCtrl::render(Inkscape::CanvasItemBuffer *buf) unsigned char *pxb = work->get_data(); // this code allow background become isolated from rendering so we can do things like outline overlay - cairo_pattern_t *pattern = _canvas->get_background_store()->cobj(); + cairo_pattern_t *pattern = _canvas->get_background_pattern()->cobj(); guint32 backcolor = ink_cairo_pattern_get_argb32(pattern); guint32 *p = _cache; for (int i = 0; i < height; ++i) { diff --git a/src/display/control/canvas-item-rect.cpp b/src/display/control/canvas-item-rect.cpp index 78f37dbe58..fec7b3a248 100644 --- a/src/display/control/canvas-item-rect.cpp +++ b/src/display/control/canvas-item-rect.cpp @@ -202,7 +202,7 @@ void CanvasItemRect::render(Inkscape::CanvasItemBuffer *buf) buf->cr->restore(); } - cairo_pattern_t *pattern = _canvas->get_background_store()->cobj(); + cairo_pattern_t *pattern = _canvas->get_background_pattern()->cobj(); guint32 backcolor = ink_cairo_pattern_get_argb32(pattern); EXTRACT_ARGB32(backcolor, ab,rb,gb,bb) diff --git a/src/ui/tools/tool-base.cpp b/src/ui/tools/tool-base.cpp index 28ef514f48..a299035192 100644 --- a/src/ui/tools/tool-base.cpp +++ b/src/ui/tools/tool-base.cpp @@ -575,7 +575,6 @@ bool ToolBase::root_handler(GdkEvent* event) { desktop->zoom_relative(event_dt, (event->button.state & GDK_SHIFT_MASK) ? 1 / zoom_inc : zoom_inc); - desktop->updateNow(); ret = TRUE; } else if (panning == event->button.button) { panning = PANNING_NONE; @@ -589,7 +588,6 @@ bool ToolBase::root_handler(GdkEvent* event) { Geom::Point const moved_w(motion_w - button_w); this->desktop->scroll_relative(moved_w); - desktop->updateNow(); ret = TRUE; } else if (zoom_rb == event->button.button) { zoom_rb = 0; @@ -747,8 +745,6 @@ bool ToolBase::root_handler(GdkEvent* event) { xp = yp = 0; ungrabCanvasEvents(); - - desktop->updateNow(); } if (panning_cursor == 1) { diff --git a/src/ui/widget/canvas-grid.cpp b/src/ui/widget/canvas-grid.cpp index a5ebe82c2f..770ae2f8de 100644 --- a/src/ui/widget/canvas-grid.cpp +++ b/src/ui/widget/canvas-grid.cpp @@ -153,7 +153,7 @@ CanvasGrid::CanvasGrid(SPDesktopWidget *dtw) void CanvasGrid::UpdateRulers() { - Geom::Rect viewbox = _dtw->desktop->get_display_area(true).bounds(); + Geom::Rect viewbox = _dtw->desktop->get_display_area().bounds(); // "true" means: Use integer values of the canvas for calculating the display area, similar // to the integer values used for positioning the grid lines. (see SPCanvas::scrollTo(), // where ix and iy are rounded integer values; these values are stored in CanvasItemBuffer->rect, diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 546bac5ba3..c58ef7153f 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -12,6 +12,7 @@ */ #include +#include #include @@ -84,13 +85,13 @@ */ struct PaintRectSetup { - Geom::IntRect canvas_rect; gint64 start_time; + Geom::IntRect canvas_rect; int max_pixels; Geom::Point mouse_loc; + bool disable_timeouts; }; - namespace Inkscape { namespace UI { namespace Widget { @@ -102,16 +103,16 @@ Canvas::Canvas() set_name("InkscapeCanvas"); // Events - add_events(Gdk::BUTTON_PRESS_MASK | - Gdk::BUTTON_RELEASE_MASK | - Gdk::ENTER_NOTIFY_MASK | - Gdk::LEAVE_NOTIFY_MASK | - Gdk::FOCUS_CHANGE_MASK | - Gdk::KEY_PRESS_MASK | - Gdk::KEY_RELEASE_MASK | - Gdk::POINTER_MOTION_MASK | - Gdk::SCROLL_MASK | - Gdk::SMOOTH_SCROLL_MASK ); + add_events(Gdk::BUTTON_PRESS_MASK | + Gdk::BUTTON_RELEASE_MASK | + Gdk::ENTER_NOTIFY_MASK | + Gdk::LEAVE_NOTIFY_MASK | + Gdk::FOCUS_CHANGE_MASK | + Gdk::KEY_PRESS_MASK | + Gdk::KEY_RELEASE_MASK | + Gdk::POINTER_MOTION_MASK | + Gdk::SCROLL_MASK | + Gdk::SMOOTH_SCROLL_MASK ); // Give _pick_event an initial definition. _pick_event.type = GDK_LEAVE_NOTIFY; @@ -126,6 +127,8 @@ Canvas::Canvas() _canvas_item_root = new Inkscape::CanvasItemGroup(nullptr); _canvas_item_root->set_name("CanvasItemGroup:Root"); _canvas_item_root->set_canvas(this); + + srand(g_get_monotonic_time()); } Canvas::~Canvas() @@ -135,21 +138,19 @@ Canvas::~Canvas() _drawing = nullptr; _in_destruction = true; - remove_idle(); - // Remove entire CanvasItem tree. delete _canvas_item_root; } /** - * Is world point inside of canvas area? + * Is world point inside canvas area? */ bool Canvas::world_point_inside_canvas(Geom::Point const &world) { Gtk::Allocation allocation = get_allocation(); - return ( (_x0 <= world.x()) && (world.x() < _x0 + allocation.get_width()) && - (_y0 <= world.y()) && (world.y() < _y0 + allocation.get_height()) ); + return ( _x0 <= world.x() && world.x() < _x0 + allocation.get_width() && + _y0 <= world.y() && world.y() < _y0 + allocation.get_height() ); } /** @@ -164,23 +165,13 @@ Canvas::canvas_to_world(Geom::Point const &point) /** * Return the area shown in the canvas in world coordinates. */ -Geom::Rect -Canvas::get_area_world() -{ - return Geom::Rect::from_xywh(_x0, _y0, _width, _height); -} - -/** - * Return the area shown the canvas in world coordinates, rounded to integer values. - */ Geom::IntRect -Canvas::get_area_world_int() +Canvas::get_area_world() { Gtk::Allocation allocation = get_allocation(); return Geom::IntRect::from_xywh(_x0, _y0, allocation.get_width(), allocation.get_height()); } - /** * Set the affine for the canvas and flag need for geometry update. */ @@ -205,7 +196,7 @@ Canvas::redraw_all() return; } _in_full_redraw = true; - _clean_region->intersect(Cairo::Region::create()); // Empty region (i.e. everything is dirty). + _clean_region = Cairo::Region::create(); // Empty region (i.e. everything is dirty). add_idle(); } @@ -258,8 +249,9 @@ Canvas::redraw_area(Geom::Coord x0, Geom::Coord y0, Geom::Coord x1, Geom::Coord redraw_area( static_cast(std::floor(std::clamp(x0, min_int, max_int))), static_cast(std::floor(std::clamp(y0, min_int, max_int))), - static_cast(std::ceil(std::clamp(x1, min_int, max_int))), - static_cast(std::ceil(std::clamp(y1, min_int, max_int)))); + static_cast(std::ceil (std::clamp(x1, min_int, max_int))), + static_cast(std::ceil (std::clamp(y1, min_int, max_int))) + ); } void @@ -268,20 +260,6 @@ Canvas::redraw_area(Geom::Rect& area) redraw_area(area.left(), area.top(), area.right(), area.bottom()); } -/** - * Immediate redraw of areas needing redrawing (don't wait for idle handler). - */ -void -Canvas::redraw_now() -{ - if (!_drawing) { - g_warning("Canvas::%s _drawing is NULL", __func__); - return; - } - - do_update(); -} - /** * Redraw after changing canvas item geometry. */ @@ -295,66 +273,24 @@ Canvas::request_update() /** * This is the first function called (after constructor) for Inkscape (not Inkview). * Scroll window so drawing point 'c' is at upper left corner of canvas. - * Complete redraw if 'clear' is true. */ void -Canvas::scroll_to(Geom::Point const &c, bool clear) +Canvas::scroll_to(Geom::Point const &c) { - int old_x0 = _x0; - int old_y0 = _y0; - - // This is the only place the _x0 and _y0 are set! - _x0 = (int) round(c[Geom::X]); // cx might be negative, so (int)(cx + 0.5) will not do! - _y0 = (int) round(c[Geom::Y]); - _window_origin = c; // Double value + int x = (int) std::round(c[Geom::X]); + int y = (int) std::round(c[Geom::Y]); - if (!_backing_store) { - // We haven't drawn anything yet! + if (x == _x0 && y == _y0) { return; } - int dx = _x0 - old_x0; - int dy = _y0 - old_y0; - - if (dx == 0 && dy == 0) { - return; // No scroll... do nothing. - } - - // See if there is any overlap between canvas before and after scrolling. - Geom::IntRect old_area = Geom::IntRect::from_xywh(old_x0, old_y0, _allocation.get_width(), _allocation.get_height()); - Geom::IntRect new_area = old_area + Geom::IntPoint(dx, dy); - bool overlap = new_area.intersects(old_area); - - if (_drawing) { - Geom::IntRect expanded = new_area; - Geom::IntPoint expansion(new_area.width()/2, new_area.height()/2); - expanded.expandBy(expansion); - _drawing->setCacheLimit(expanded, false); - } - - if (clear || !overlap) { - redraw_all(); - return; // Check if this is OK - } - - // Copy backing store - shift_content(Geom::IntPoint(dx, dy), _backing_store); - if (_split_mode != Inkscape::SplitMode::NORMAL || _drawing->outlineOverlay()) { - shift_content(Geom::IntPoint(dx, dy), _outline_store); - } - - // Mark surface to redraw (everything outside clean region). - Cairo::RectangleInt crect = { _x0, _y0, _allocation.get_width(), _allocation.get_height() }; - _clean_region->intersect(crect); // Shouldn't the clean region be reset and then this added? + _x0 = x; + _y0 = y; - // Scroll without zoom: redraw only newly exposed areas. - if (get_realized()) { - auto window = get_window(); - window->scroll(-dx, -dy); // Triggers of newly exposed region. - } + add_idle(); + queue_draw(); - auto grid = dynamic_cast(get_parent()); - if (grid) { + if (auto grid = dynamic_cast(get_parent())) { grid->UpdateRulers(); } } @@ -370,7 +306,6 @@ Canvas::set_background_color(guint32 rgba) double b = SP_RGBA32_B_F(rgba); _background = Cairo::SolidPattern::create_rgb(r, g, b); - _background_is_checkerboard = false; redraw_all(); } @@ -383,7 +318,6 @@ Canvas::set_background_checkerboard(guint32 rgba) { auto pattern = ink_cairo_pattern_create_checkerboard(rgba); _background = Cairo::RefPtr(new Cairo::Pattern(pattern)); - _background_is_checkerboard = true; redraw_all(); } @@ -465,13 +399,13 @@ Canvas::canvas_item_clear(Inkscape::CanvasItem* item) // ============== Protected Functions ============== void -Canvas::get_preferred_width_vfunc (int& minimum_width, int& natural_width) const +Canvas::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const { minimum_width = natural_width = 256; } void -Canvas::get_preferred_height_vfunc (int& minimum_height, int& natural_height) const +Canvas::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const { minimum_height = natural_height = 256; } @@ -481,7 +415,7 @@ bool Canvas::on_scroll_event(GdkEventScroll *scroll_event) { // Scroll canvas and in Select Tool, cycle selection through objects under cursor. - return emit_event(reinterpret_cast(scroll_event)); + return emit_event(reinterpret_cast(scroll_event)); } // Our own function that combines press and release. @@ -505,7 +439,6 @@ Canvas::on_button_event(GdkEventButton *button_event) default: mask = 0; // Buttons can range at least to 9 but mask defined only to 5. } - bool retval = false; switch (button_event->type) { case GDK_BUTTON_PRESS: @@ -529,7 +462,7 @@ Canvas::on_button_event(GdkEventButton *button_event) // Fallthrough case GDK_3BUTTON_PRESS: - // Pick the current item as if the button were not press and then process event. + // Pick the current item as if the button were not pressed and then process event. _state = button_event->state; pick_current_item(reinterpret_cast(button_event)); @@ -632,10 +565,10 @@ Canvas::on_motion_notify_event(GdkEventMotion *motion_event) if (_desktop) { // Check if we are near the edge. If so, revert to normal mode. if (_split_mode == Inkscape::SplitMode::SPLIT && _split_dragging) { - if (cursor_position.x() < 5 || - cursor_position.y() < 5 || - cursor_position.x() - _allocation.get_width() > -5 || - cursor_position.y() - _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_mode = Inkscape::SplitMode::NORMAL; @@ -737,23 +670,11 @@ Canvas::on_motion_notify_event(GdkEventMotion *motion_event) } // End if(desktop) _state = motion_event->state; - pick_current_item(reinterpret_cast(motion_event)); - bool status = emit_event(reinterpret_cast(motion_event)); + pick_current_item(reinterpret_cast(motion_event)); + bool status = emit_event(reinterpret_cast(motion_event)); return status; } -/** - * Resize handler - */ -void Canvas::on_size_allocate(Gtk::Allocation &allocation) -{ - parent_type::on_size_allocate(allocation); - - assert(allocation == get_allocation()); - _width = allocation.get_width(); - _height = allocation.get_height(); -} - /* * The on_draw() function is called whenever Gtk wants to update the window. This function: * @@ -771,33 +692,18 @@ void Canvas::on_size_allocate(Gtk::Allocation &allocation) * 5. Calls add_idle() to update the drawing if necessary. */ bool -Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) +Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) { // sp_canvas_item_recursive_print_tree(0, _root); // canvas_item_print_tree(_canvas_item_root); - // This function should be the only place _allocation is redefined (except in on_realize())! - Gtk::Allocation allocation = get_allocation(); - int device_scale = get_scale_factor(); - - // This is the only place we should initialize _backing_store! (Elsewhere, it's recreated.) - if (!(_allocation == allocation) || _device_scale != device_scale) { // "!=" for allocation not defined! - _allocation = allocation; - _device_scale = device_scale; - - // Create new stores and copy/shift contents. - shift_content(Geom::IntPoint(0, 0), _backing_store); - shift_content(Geom::IntPoint(0, 0), _outline_store); - - // Clip the clean region to the new allocation - Cairo::RectangleInt clip = { _x0, _y0, _allocation.get_width(), _allocation.get_height() }; - _clean_region->intersect(clip); - } + std::cout << "on_draw\n"; - assert(_backing_store && _outline_store); + assert(_backing_store/* && _outline_store*/); assert(_drawing); - // This is the only place the widget content is drawn! + // todo: can minutely optimise this by only running the first part of on_idle() + on_idle(); // Blit background (e.g. checkerboard). cr->save(); @@ -805,11 +711,25 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) cr->set_source(_background); cr->paint(); cr->restore(); + // todo: remember the solid-colour optimisation to go into this // Blit from the backing store, without regard for the clean region. - cr->set_source(_backing_store, 0, 0); + cr->set_source(_backing_store, _store_rect.left() - _x0, _store_rect.top() - _y0); cr->paint(); + // Paint unclean regions in red + auto reg = Cairo::Region::create( Cairo::RectangleInt{ _x0, _y0, get_allocation().get_width(), get_allocation().get_height() } ); + reg->subtract(_clean_region); + + cr->set_source_rgba(1, 0, 0, 0.07); + for (int i = 0; i < reg->get_num_rectangles(); i++) + { + auto rect = reg->get_rectangle(i); + cr->rectangle(rect.x - _x0, rect.y - _y0, rect.width, rect.height); + cr->fill(); + } + +/* // Draw overlay if required. if (_drawing->outlineOverlay()) { @@ -898,13 +818,13 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) cr->restore(); } cr->restore(); - } + }*/ // static int i = 0; // ++i; // std::string file = "on_draw_" + std::to_string(i) + ".png"; // _backing_store->write_to_png(file); - +/* // This whole section is just to determine if we call add_idle! auto dirty_region = Cairo::Region::create(); @@ -926,7 +846,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context>& cr) if (!dirty_region->empty()) { add_idle(); - } + }*/ return true; } @@ -945,7 +865,7 @@ Canvas::add_idle() return; } - if (get_realized() && !_idle_connection.connected()) { + if (!_idle_connection.connected()) { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); guint redrawPriority = prefs->getIntLimited("/options/redrawpriority/value", G_PRIORITY_HIGH_IDLE, G_PRIORITY_HIGH_IDLE, G_PRIORITY_DEFAULT_IDLE); if (_in_full_redraw) { @@ -954,15 +874,28 @@ Canvas::add_idle() } // G_PRIORITY_HIGH_IDLE = 100, G_PRIORITY_DEFAULT_IDLE = 200: Higher number => lower priority. - _idle_connection = Glib::signal_idle().connect(sigc::mem_fun(*this, &Canvas::on_idle), redrawPriority); + // TEMP HACK: the high priority idle screws with my redraw implementation, so ignoring it for now + _idle_connection = Glib::signal_idle().connect(sigc::mem_fun(*this, &Canvas::on_idle), G_PRIORITY_DEFAULT_IDLE); } } -// Probably not needed. -void -Canvas::remove_idle() +auto +geom_to_cairo(Geom::IntRect rect) +{ + return Cairo::RectangleInt { rect.left(), rect.top(), rect.width(), rect.height() }; +} + +auto +cairo_to_geom(Cairo::RectangleInt rect) +{ + return Geom::IntRect::from_xywh(rect.x, rect.y, rect.width, rect.height); +} + +auto +distSq(const Geom::IntPoint pt, const Geom::IntRect &rect) { - _idle_connection.disconnect(); + auto v = rect.clamp(pt) - pt; + return v.x() * v.x() + v.y() * v.y(); } bool @@ -972,118 +905,103 @@ Canvas::on_idle() std::cerr << "Canvas::on_idle: Called after canvas destroyed!" << std::endl; } - if (!_drawing) { - return false; // Disconnect + // Quit idle process if not supposed to be drawing + if (!_drawing || _drawing_disabled) { + return false; } - bool done = do_update(); - int n_rects = _clean_region->get_num_rectangles(); - - if (n_rects > 1) { - done = false; + // Ensure geometry is up to date + assert(_canvas_item_root); + if (_need_update) { + _canvas_item_root->update(_affine); + _need_update = false; } - return !done; -} + // Get canvas rectangle in world coordinates + const auto canvas_rect = Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() ); -/* - * Paints if drawable (widget mapped and visible). - * Otherwise picks (which makes no sense). - * Return true if done. - */ -bool -Canvas::do_update() -{ - assert(_canvas_item_root); - assert(_drawing); + // Assert that _clean_region is a subregion of _store_rect + auto tmp = _clean_region->copy(); + tmp->subtract(geom_to_cairo(_store_rect)); + assert(tmp->empty()); - if (_drawing_disabled) { - return true; - } + // Ensure store contains canvas_rect + const auto pad = Geom::IntPoint(200, 200); + const auto device_scale = get_scale_factor(); - if (get_is_drawable()) { - // We're mapped and visible. - if (_need_update) { - _canvas_item_root->update(_affine); - _need_update = false; - } - return paint(); - } + if (!_backing_store || _device_scale != device_scale || !_store_rect.intersects(canvas_rect)) + { + // Recreate the store, using the same memory if possible + _store_rect = Geom::IntRect::from_xywh( _x0, _y0, canvas_rect.width(), canvas_rect.height() ); + _store_rect.expandBy(pad); + _device_scale = device_scale; + if (!_backing_store || _backing_store->get_width() != _store_rect.width() * _device_scale || _backing_store->get_height() != _store_rect.height() * _device_scale) + _backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, _store_rect.width() * _device_scale, _store_rect.height() * _device_scale); + _clean_region = Cairo::Region::create(); - // TODO: This makes no sense as normally we wouldn't reach here. - // Pick current item: - while (_need_repick) { - _need_repick = false; - pick_current_item(&_pick_event); + std::cout << "Recreated store" << std::endl; } + else if (!_store_rect.contains(canvas_rect)) + { + // Create new store, copy usable content across, set as new store + auto store_rect = Geom::IntRect::from_xywh( _x0, _y0, canvas_rect.width(), canvas_rect.height() ); + store_rect.expandBy(pad); + auto backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, store_rect.width() * _device_scale, store_rect.height() * _device_scale); - return true; // FIXME?? -} - + auto shift = store_rect.min() - _store_rect.min(); + auto reuse_rect = store_rect & _store_rect; + auto cr = Cairo::Context::create(backing_store); -/* - * Paint the "dirty" areas of the canvas, usually multiple rectangles. - */ -bool -Canvas::paint() -{ - if (_need_update) { - std::cerr << "Canvas::Paint: called while needing update!" << std::endl; - } + // copy contents of store + assert(reuse_rect); + cr->save(); + cr->rectangle(reuse_rect->left() - store_rect.left(), reuse_rect->top() - store_rect.top(), reuse_rect->width(), reuse_rect->height()); + cr->clip(); + cr->set_source(_backing_store, -shift.x(), -shift.y()); + cr->paint(); + cr->restore(); - Cairo::RectangleInt crect = { _x0, _y0, _allocation.get_width(), _allocation.get_height() }; - auto draw_region = Cairo::Region::create(crect); - draw_region->subtract(_clean_region); + _store_rect = store_rect; + _backing_store = std::move(backing_store); + _clean_region->intersect(geom_to_cairo(_store_rect)); - int n_rects = draw_region->get_num_rectangles(); - for (int i = 0; i < n_rects; ++i) { - auto rect = draw_region->get_rectangle(i); - if (!paint_rect(rect)) { - // Aborted - return false; - }; + std::cout << "Partially recreated store" << std::endl; } - return true; -} + assert(_store_rect.contains(canvas_rect)); -/* - * Paint a rectangular area. - * rect: The rectangle to paint (in widget coordinates). - */ -bool -Canvas::paint_rect(Cairo::RectangleInt& rect) -{ - // Find window rectangle in 'world coordinates'. - Geom::IntRect canvas_rect = Geom::IntRect::from_xywh(_x0, _y0, _allocation.get_width(), _allocation.get_height()); - Geom::IntRect paint_rect = Geom::IntRect::from_xywh(rect.x, rect.y, rect.width, rect.height); - Geom::OptIntRect area = paint_rect & canvas_rect; + // Get region that requires painting + auto region = Cairo::Region::create(geom_to_cairo(canvas_rect)); + region->subtract(_clean_region); - // Don't stop idle process if empty. - if (!area || area->hasZeroArea()) { - return true; + // Get mouse position in canvas space + Geom::IntPoint mouse_loc; + if (auto window = get_window()) { + int x; + int y; + Gdk::ModifierType mask; + window->get_device_position(Gdk::Display::get_default()->get_default_seat()->get_pointer(), x, y, mask); + mouse_loc = Geom::IntPoint(_x0 + x, _y0 + y); + } + else { + mouse_loc = canvas_rect.midpoint(); } - // Get cursor position - auto const display = Gdk::Display::get_default(); - auto const seat = display->get_default_seat(); - auto const device = seat->get_pointer(); - - int x = 0; - int y = 0; - Gdk::ModifierType mask; - auto window = get_window(); - if (window) { - window->get_device_position(device, x, y, mask); + // Obtain rectangles list sorted by distance from mouse + std::vector rects; + rects.reserve(region->get_num_rectangles()); + for (int i = 0; i < region->get_num_rectangles(); i++) { + rects.emplace_back(cairo_to_geom(region->get_rectangle(i))); } + std::sort(rects.begin(), rects.end(), [&] (const Geom::IntRect &a, const Geom::IntRect &b) {return distSq(mouse_loc, a) < distSq(mouse_loc, b);}); + // Set up painting info to pass down PaintRectSetup setup; setup.canvas_rect = canvas_rect; - setup.mouse_loc = Geom::Point(_x0 + x, _y0 + y); - setup.start_time = g_get_monotonic_time(); + setup.mouse_loc = mouse_loc; - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - unsigned tile_multiplier = prefs->getIntLimited("/options/rendering/tile-multiplier", 16, 1, 512); + auto prefs = Inkscape::Preferences::get(); + auto tile_multiplier = prefs->getIntLimited("/options/rendering/tile-multiplier", 16, 1, 512); if (_render_mode != Inkscape::RenderMode::OUTLINE) { // Can't be too small or large gradient will be rerendered too many times! setup.max_pixels = 65536 * tile_multiplier; @@ -1092,74 +1010,118 @@ Canvas::paint_rect(Cairo::RectangleInt& rect) setup.max_pixels = 262144; } - return paint_rect_internal(&setup, paint_rect); -} + // Begin painting + setup.start_time = g_get_monotonic_time(); + setup.disable_timeouts = _forced_redraw_limit != -1 && _forced_redraw_count >= _forced_redraw_limit; + setup.disable_timeouts = false; // TEMP HACK: I WANT TO INVESTIGATE BEHAVIOUR WITHOUT THIS INTERFERING + for (const auto &rect : rects) { + auto area = rect & canvas_rect; -/* - * Returns true on successful rendering of rectangle (unless error). - * Returns false if rectangle has no area or if timed out. - * Queues Gtk redraw of widget. - */ -bool -Canvas::paint_rect_internal(PaintRectSetup const *setup, Geom::IntRect const &this_rect) -{ - if (!_drawing) { - std::cerr << "Canvas::paint_rect_internal: no CanvasItemDrawing!" << std::endl; - return false; + if (!area || area->hasZeroArea()) { + continue; + } + + if (!paint_rect_internal(setup, rect)) { + // Timed out. Temporarily return to idle loop, and come back here if still idle. + std::cout << "timed out: " << g_get_monotonic_time() - setup.start_time << " us \n"; + _forced_redraw_count++; + return true; + } } - gint64 now = g_get_monotonic_time(); - gint64 elapsed = now - setup->start_time; - - // Allow only very fast buffers to be run together; - // as soon as the total redraw time exceeds 1ms, cancel; - // this returns control to the idle loop and allows Inkscape to process user input - // (potentially interrupting the redraw); as soon as Inkscape has some more idle time, - if (elapsed > 1000) { - // Interrupting redraw isn't always good. - // For example, when you drag one node of a big path, only the buffer containing - // the mouse cursor will be redrawn again and again, and the rest of the path - // will remain stale because Inkscape never has enough idle time to redraw all - // of the screen. To work around this, such operations set a forced_redraw_limit > 0. - // If this limit is set, and if we have aborted redraw more times than is allowed, - // interrupting is blocked and we're forced to redraw full screen once - // (after which we can again interrupt forced_redraw_limit times). - if (_forced_redraw_limit < 0 || - _forced_redraw_count < _forced_redraw_limit) { - - if (_forced_redraw_limit != -1) { - _forced_redraw_count++; - } - return false; + // Check if suppressed a time out, and adjust counter if so + if (setup.disable_timeouts) + { + auto now = g_get_monotonic_time(); + auto elapsed = now - setup.start_time; + if (elapsed > 1000) { + // Timed out + std::cout << "ignore timeout: " << g_get_monotonic_time() - setup.start_time << " us \n"; + _forced_redraw_count = 0; } - _forced_redraw_count = 0; } + // todo: check clean region is what it should be + + std::cout << "finished drawing\n"; + return false; +} + +/* + * Returns false to bail out in the event of a timeout. + * Queues Gtk redraw of widget. + */ +bool +Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &this_rect) +{ // Find optimal buffer dimension int bw = this_rect.width(); int bh = this_rect.height(); if (bw < 1 || bh < 1) { // Nothing to draw! - return false; // Don't idle stop process if area is empty. + return true; + } + + /* + // Uncomment for artificial redraw slowness fun - you won't regret it! + if (bw > bh || bw > 10) { + int mid = this_rect[Geom::X].middle(); + + Geom::IntRect lo, hi; + lo = Geom::IntRect(this_rect.left(), this_rect.top(), mid, this_rect.bottom()); + hi = Geom::IntRect(mid, this_rect.top(), this_rect.right(), this_rect.bottom()); + + if (setup.mouse_loc[Geom::X] < mid) { + // Always paint towards the mouse first + return paint_rect_internal(setup, lo) + && paint_rect_internal(setup, hi); + } else { + return paint_rect_internal(setup, hi) + && paint_rect_internal(setup, lo); + } + } else if (bh > bw && bh > 10) { + int mid = this_rect[Geom::Y].middle(); + + Geom::IntRect lo, hi; + lo = Geom::IntRect(this_rect.left(), this_rect.top(), this_rect.right(), mid ); + hi = Geom::IntRect(this_rect.left(), mid, this_rect.right(), this_rect.bottom()); + + if (setup.mouse_loc[Geom::Y] < mid) { + // Always paint towards the mouse first + return paint_rect_internal(setup, lo) + && paint_rect_internal(setup, hi); + } else { + return paint_rect_internal(setup, hi) + && paint_rect_internal(setup, lo); + } } + */ - if (bw * bh < setup->max_pixels) { + if (bw * bh < setup.max_pixels) { // We are small enough! + if (!setup.disable_timeouts) { + auto now = g_get_monotonic_time(); + auto elapsed = now - setup.start_time; + if (elapsed > 1000) { + return false; + } + } + _drawing->setRenderMode(_render_mode); _drawing->setColorMode(_color_mode); - paint_single_buffer(this_rect, setup->canvas_rect, _backing_store); - bool outline_overlay = _drawing->outlineOverlay(); + paint_single_buffer(this_rect, setup.canvas_rect, _backing_store); + /*bool outline_overlay = _drawing->outlineOverlay(); if (_split_mode != Inkscape::SplitMode::NORMAL || outline_overlay) { _drawing->setRenderMode(Inkscape::RenderMode::OUTLINE); - paint_single_buffer(this_rect, setup->canvas_rect, _outline_store); + paint_single_buffer(this_rect, setup.canvas_rect, _outline_store); if (outline_overlay) { _drawing->setRenderMode(Inkscape::RenderMode::OUTLINE_OVERLAY); } - } + }*/ Cairo::RectangleInt crect = { this_rect.left(), this_rect.top(), this_rect.width(), this_rect.height() }; _clean_region->do_union( crect ); @@ -1192,7 +1154,7 @@ Canvas::paint_rect_internal(PaintRectSetup const *setup, Geom::IntRect const &th lo = Geom::IntRect(this_rect.left(), this_rect.top(), mid, this_rect.bottom()); hi = Geom::IntRect(mid, this_rect.top(), this_rect.right(), this_rect.bottom()); - if (setup->mouse_loc[Geom::X] < mid) { + if (setup.mouse_loc[Geom::X] < mid) { // Always paint towards the mouse first return paint_rect_internal(setup, lo) && paint_rect_internal(setup, hi); @@ -1206,7 +1168,7 @@ Canvas::paint_rect_internal(PaintRectSetup const *setup, Geom::IntRect const &th lo = Geom::IntRect(this_rect.left(), this_rect.top(), this_rect.right(), mid ); hi = Geom::IntRect(this_rect.left(), mid, this_rect.right(), this_rect.bottom()); - if (setup->mouse_loc[Geom::Y] < mid) { + if (setup.mouse_loc[Geom::Y] < mid) { // Always paint towards the mouse first return paint_rect_internal(setup, lo) && paint_rect_internal(setup, hi); @@ -1230,17 +1192,19 @@ Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Geom::IntRect const if (!store) { std::cerr << "Canvas::paint_single_buffer: store not created!" << std::endl; return; - // Maybe store not created! + // Maybe store not created! - todo: check if this can actually happen } - Inkscape::CanvasItemBuffer buf(paint_rect, canvas_rect, _device_scale); - // Make sure the following code does not go outside of store's data assert(store->get_format() == Cairo::FORMAT_ARGB32); - assert(paint_rect.left() - _x0 >= 0); - assert(paint_rect.top() - _y0 >= 0); - assert(paint_rect.right() - _x0 <= store->get_width()); - assert(paint_rect.bottom() - _y0 <= store->get_height()); + assert(_store_rect.contains(paint_rect)); + + /*auto cr = Cairo::Context::create(store); + cr->set_source_rgba((rand() % 255) / 255.0, (rand() % 255) / 255.0, (rand() % 255) / 255.0, 0.2); + cr->rectangle(paint_rect.left() - _store_rect.left(), paint_rect.top() - _store_rect.top(), paint_rect.width(), paint_rect.height()); + cr->fill();*/ + + Inkscape::CanvasItemBuffer buf(paint_rect, _store_rect, _device_scale); // Create temporary surface that draws directly to store. store->flush(); @@ -1261,17 +1225,19 @@ Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Geom::IntRect const double x_scale = 1.0; double y_scale = 1.0; cairo_surface_get_device_scale(store->cobj(), &x_scale, &y_scale); // No C++ API! - assert (_device_scale == (int)x_scale); - assert (_device_scale == (int)y_scale); + assert (_device_scale == (int) x_scale); + assert (_device_scale == (int) y_scale); + // Move to the correct row. - data += stride * (paint_rect.top() - _y0) * (int)y_scale; + data += stride * (paint_rect.top() - _store_rect.top()) * (int)y_scale; // Move to the correct column. - data += 4 * (paint_rect.left() - _x0) * (int)x_scale; + data += 4 * (paint_rect.left() - _store_rect.left()) * (int)x_scale; auto imgs = Cairo::ImageSurface::create(data, Cairo::FORMAT_ARGB32, paint_rect.width() * _device_scale, paint_rect.height() * _device_scale, stride); + cairo_surface_set_device_scale(imgs->cobj(), _device_scale, _device_scale); // No C++ API! auto cr = Cairo::Context::create(imgs); @@ -1330,56 +1296,8 @@ Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Geom::IntRect const // cr->set_source_rgba(0.0, 0.0, 0.5, 1.0); // cr->stroke(); // cr->restore(); - - // TODO Check... the rest duplicates a call after this function returns. - Cairo::RectangleInt crect = { paint_rect.left(), paint_rect.top(), paint_rect.width(), paint_rect.height() }; - _clean_region->do_union( crect ); - - queue_draw_area(paint_rect.left() - _x0, paint_rect.top() - _y0, paint_rect.width(), paint_rect.height()); -} - - -// Shift backing store (when canvas scrolled or size changed). -void -Canvas::shift_content(Geom::IntPoint shift, Cairo::RefPtr &store) -{ - Cairo::RefPtr<::Cairo::ImageSurface> new_store = - Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, - _allocation.get_width() * _device_scale, - _allocation.get_height() * _device_scale); - - cairo_surface_set_device_scale(new_store->cobj(), _device_scale, _device_scale); // No C++ API! - - // Copy the old store contents to new backing store. - auto cr = Cairo::Context::create(new_store); - - // Paint background - cr->set_operator(Cairo::Operator::OPERATOR_SOURCE); - cr->set_source(_background); - cr->paint(); - - if (store) { - // Copy old background unshifted (reduces sensation of flicker while waiting for rendering newly exposed area). - cr->set_source(store, 0, 0); - cr->paint(); - - // Copy old background - cr->rectangle(-shift.x(), -shift.y(), _allocation.get_width(), _allocation.get_height()); - cr->clip(); - cr->translate(-shift.x(), -shift.y()); - cr->set_source(store, 0, 0); - cr->paint(); - } - - store = new_store; - - // static int i = 0; - // ++i; - // std::string file = "shift_content_" + std::to_string(i) + ".png"; - // _store->write_to_png(file); } - // Sets clip path for Split and X-Ray modes. void Canvas::add_clippath(const Cairo::RefPtr& cr) { @@ -1387,8 +1305,8 @@ Canvas::add_clippath(const Cairo::RefPtr& cr) { Inkscape::Preferences *prefs = Inkscape::Preferences::get(); double radius = prefs->getIntLimited("/options/rendering/xray-radius", 100, 1, 1500); - double width = _allocation.get_width(); - double height = _allocation.get_height(); + double width = get_allocation().get_width(); + double height = get_allocation().get_height(); double sx = _split_position.x(); double sy = _split_position.y(); diff --git a/src/ui/widget/canvas.h b/src/ui/widget/canvas.h index a902088909..25c145e981 100644 --- a/src/ui/widget/canvas.h +++ b/src/ui/widget/canvas.h @@ -57,8 +57,7 @@ public: // Geometry bool world_point_inside_canvas(Geom::Point const &world); // desktop-events.cpp Geom::Point canvas_to_world(Geom::Point const &window); - Geom::Rect get_area_world(); - Geom::IntRect get_area_world_int(); // Shouldn't really need this, only used for rulers. + Geom::IntRect get_area_world(); void set_affine(Geom::Affine const &affine); Geom::Affine get_affine() { return _affine; } @@ -66,20 +65,19 @@ public: void set_drawing(Inkscape::Drawing *drawing) { _drawing = drawing; } void redraw_all(); // Draw entire surface during idle. void redraw_area(Geom::Rect& area); // Draw specified area during idle. - void redraw_now(); // Draw areas needing update immediately. void request_update(); // Draw after updating canvas items. - void scroll_to(Geom::Point const &c, bool clear); + void scroll_to(Geom::Point const &c); void set_background_color(guint32 rgba); void set_background_checkerboard(guint32 rgba = 0xC4C4C4FF); void set_drawing_disabled(bool disable) { _drawing_disabled = disable; } // Disable during path ops, etc. - bool is_dragging() {return _is_dragging; } // selection-chemistry.cpp + bool is_dragging() { return _is_dragging; } // selection-chemistry.cpp // Rendering modes void set_render_mode(Inkscape::RenderMode mode); - void set_color_mode(Inkscape::ColorMode mode); - void set_split_mode(Inkscape::SplitMode mode); + void set_color_mode( Inkscape::ColorMode mode); + void set_split_mode( Inkscape::SplitMode mode); void set_split_direction(Inkscape::SplitDirection dir); Inkscape::RenderMode get_render_mode() { return _render_mode; } Inkscape::ColorMode get_color_mode() { return _color_mode; } @@ -94,7 +92,7 @@ public: bool get_cms_active() { return _cms_active; } Cairo::RefPtr get_backing_store() { return _backing_store; } // Background rotation preview - Cairo::RefPtr get_background_store() { return _background; } + Cairo::RefPtr get_background_pattern() { return _background; } // For a GTK bug (see SelectedStyle::on_opacity_changed()). void forced_redraws_start(int count, bool reset = true); @@ -105,18 +103,19 @@ public: Inkscape::CanvasItem *get_current_canvas_item() { return _current_canvas_item; } void set_current_canvas_item(Inkscape::CanvasItem *item) { - _current_canvas_item = item; } + _current_canvas_item = item; + } Inkscape::CanvasItem *get_grabbed_canvas_item() { return _grabbed_canvas_item; } void set_grabbed_canvas_item(Inkscape::CanvasItem *item, Gdk::EventMask mask) { _grabbed_canvas_item = item; _grabbed_event_mask = mask; } - void set_need_repick(bool repick = true) { _need_repick = repick; } - void canvas_item_clear(Inkscape::CanvasItem *item); + void set_need_repick(bool repick = true) { _need_repick = repick; } + void canvas_item_clear(Inkscape::CanvasItem *item); // Events - void set_all_enter_events(bool on) { _all_enter_events = on; } + void set_all_enter_events(bool on) { _all_enter_events = on; } protected: @@ -135,7 +134,6 @@ protected: bool on_key_press_event( GdkEventKey *key_event ) override; bool on_key_release_event( GdkEventKey *key_event ) override; bool on_motion_notify_event( GdkEventMotion *motion_event) override; - void on_size_allocate(Gtk::Allocation &) override; // Painting bool on_draw(const Cairo::RefPtr& cr) override; @@ -146,7 +144,6 @@ private: // ======== Functions ======= void add_idle(); - void remove_idle(); // Not needed? bool on_idle(); // Drawing (internal overloads) @@ -156,14 +153,10 @@ private: // Painting // In order they are called in painting. - bool do_update(); bool paint(); - bool paint_rect(Cairo::RectangleInt& rect); - bool paint_rect_internal(PaintRectSetup const *setup, Geom::IntRect const &this_rect); + bool paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &this_rect); void paint_single_buffer(Geom::IntRect const &paint_rect, Geom::IntRect const &canvas_rect, Cairo::RefPtr &store); - - void shift_content(Geom::IntPoint shift, Cairo::RefPtr &store); void add_clippath(const Cairo::RefPtr& cr); void set_cursor(); @@ -172,33 +165,26 @@ private: bool emit_event(GdkEvent *event); // ==== Signal callbacks ==== - sigc::connection _idle_connection; // Probably not needed (automatically disconnects). + sigc::connection _idle_connection; // ====== Data members ======= // Structure - SPDesktop * _desktop = nullptr; + SPDesktop *_desktop = nullptr; // Geometry - int _x0 = 0; ///< World coordinate of the leftmost pixels of window. - int _y0 = 0; ///< World coordinate of the topmost pixels of window. - Geom::Point _window_origin; ///< World coordinate of the upper-leftmost pixel of window. - Geom::Affine _affine; // Only used for canvas items at moment. - bool _in_full_redraw = false; - int _device_scale = 1; ///< Scale for high DPI montiors. Probably should be double. - Gtk::Allocation _allocation; ///< Canvas allocation, save so we know when it changes. - - int _width = 0; ///< Canvas width, tracked by on_size_allocate - int _height = 0; ///< Canvas height, tracked by on_size_allocate + int _x0 = 0, _y0 = 0; ///< Coordinates of top-left pixel of canvas view within canvas. + Geom::Affine _affine; // Only used for canvas items at the moment. + bool _in_full_redraw = false; // Hack used to lower idle priority for full redraws. // Event handling/item picking - GdkEvent _pick_event; ///< Event used to find currently selected item. - bool _need_repick = true; ///< ? - bool _in_repick = false; ///< Used internally by pick_current_item(). - bool _left_grabbed_item = false; ///< ? - bool _all_enter_events = false; ///< Keep all enter events. Only set true in connector-tool.cpp. - bool _is_dragging = false; ///< Used in selection-chemistry to block undo/redo. - int _state = 0; ///< Last know modifier state (SHIFT, CTRL, etc.). + GdkEvent _pick_event; ///< Event used to find currently selected item. + bool _need_repick = true; ///< ? + bool _in_repick = false; ///< Used internally by pick_current_item(). + bool _left_grabbed_item = false; ///< ? + bool _all_enter_events = false; ///< Keep all enter events. Only set true in connector-tool.cpp. + bool _is_dragging = false; ///< Used in selection-chemistry to block undo/redo. + int _state = 0; ///< Last know modifier state (SHIFT, CTRL, etc.). Inkscape::CanvasItem *_current_canvas_item = nullptr; ///< Item containing cursor, nullptr if none. Inkscape::CanvasItem *_current_canvas_item_new = nullptr; ///< Item to become _current_item, nullptr if none. @@ -235,15 +221,14 @@ private: // ======= CAIRO ======= ... Keep in one place - /// Image surface storing the content of the widget. - Cairo::RefPtr _backing_store; ///< The canvas image content. We draw to this then blit. - Cairo::RefPtr _outline_store; ///< The outline image if we are in split/x-ray mode. - - Cairo::RefPtr _background; ///< The background of the image. - bool _background_is_checkerboard = false; - - Cairo::RefPtr _clean_region; ///< Area of widget that has up-to-date content. + /// Image surface storing a rendered part of the canvas + Cairo::RefPtr _backing_store; ///< Canvas content. + Cairo::RefPtr _outline_store; ///< Canvas outline content; only exists in split/x-ray mode. + Geom::IntRect _store_rect; ///< Rectangle of the store in world space. + Cairo::RefPtr _clean_region; ///< Subregion of store with up-to-date content. + int _device_scale = 1; ///< Scale for high DPI montiors. Probably should be double. + Cairo::RefPtr _background; ///< The background of the widget. // Used to update CanvasItemCtrl's when size changed. class CanvasPrefObserver : public Inkscape::Preferences::Observer { -- GitLab From 0e1fe9a5510e88a13056f4b09e4597c9cadd1381 Mon Sep 17 00:00:00 2001 From: PBS Date: Sun, 9 Jan 2022 16:07:05 +0900 Subject: [PATCH 02/35] PIMPL Canvas, possibly temporarily, to save me from recompiles. --- src/ui/widget/canvas.cpp | 105 +++++++++++++++++++++++---------------- src/ui/widget/canvas.h | 23 ++++----- 2 files changed, 71 insertions(+), 57 deletions(-) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index c58ef7153f..e3a13aa3a9 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -84,6 +84,10 @@ * them "externally" (e.g. gradient CanvasItemCurves). */ +namespace Inkscape { +namespace UI { +namespace Widget { + struct PaintRectSetup { gint64 start_time; Geom::IntRect canvas_rect; @@ -92,13 +96,21 @@ struct PaintRectSetup { bool disable_timeouts; }; -namespace Inkscape { -namespace UI { -namespace Widget { +class CanvasPrivate +{ +private: + + friend class Canvas; + Cairo::RefPtr _backing_store; ///< Canvas content. + Cairo::RefPtr _outline_store; ///< Canvas outline content; only exists in split/x-ray mode. + Geom::IntRect _store_rect; ///< Rectangle of the store in world space. + Cairo::RefPtr _clean_region; ///< Subregion of store with up-to-date content. + int _device_scale = 1; ///< Scale for high DPI montiors. Probably should be double. +}; Canvas::Canvas() - : _size_observer(this, "/options/grabsize/value") + : _size_observer(this, "/options/grabsize/value"), d(std::make_unique()) { set_name("InkscapeCanvas"); @@ -120,7 +132,7 @@ Canvas::Canvas() _pick_event.crossing.y = 0; // Drawing - _clean_region = Cairo::Region::create(); + d->_clean_region = Cairo::Region::create(); _background = Cairo::SolidPattern::create_rgb(1.0, 1.0, 1.0); @@ -196,7 +208,7 @@ Canvas::redraw_all() return; } _in_full_redraw = true; - _clean_region = Cairo::Region::create(); // Empty region (i.e. everything is dirty). + d->_clean_region = Cairo::Region::create(); // Empty region (i.e. everything is dirty). add_idle(); } @@ -233,7 +245,7 @@ Canvas::redraw_area(int x0, int y0, int x1, int y1) y1 = std::clamp(y1, min_coord, max_coord); Cairo::RectangleInt crect = { x0, y0, x1-x0, y1-y0 }; - _clean_region->subtract(crect); + d->_clean_region->subtract(crect); add_idle(); } @@ -363,6 +375,11 @@ Canvas::set_split_direction(Inkscape::SplitDirection dir) } } +Cairo::RefPtr Canvas::get_backing_store() +{ + return d->_backing_store; +} + void Canvas::forced_redraws_start(int count, bool reset) { @@ -627,7 +644,7 @@ Canvas::on_motion_notify_event(GdkEventMotion *motion_event) return true; } - if (Geom::distance(cursor_position, _split_position) < 20 * _device_scale) { + if (Geom::distance(cursor_position, _split_position) < 20 * d->_device_scale) { // We're hovering over circle, figure out which direction we are in. if (difference.y() - difference.x() > 0) { @@ -645,12 +662,12 @@ Canvas::on_motion_notify_event(GdkEventMotion *motion_event) } } else if (_split_direction == Inkscape::SplitDirection::NORTH || _split_direction == Inkscape::SplitDirection::SOUTH) { - if (std::abs(difference.y()) < 3 * _device_scale) { + if (std::abs(difference.y()) < 3 * d->_device_scale) { // We're hovering over horizontal line hover_direction = Inkscape::SplitDirection::HORIZONTAL; } } else { - if (std::abs(difference.x()) < 3 * _device_scale) { + if (std::abs(difference.x()) < 3 * d->_device_scale) { // We're hovering over vertical line hover_direction = Inkscape::SplitDirection::VERTICAL; } @@ -699,7 +716,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) std::cout << "on_draw\n"; - assert(_backing_store/* && _outline_store*/); + assert(d->_backing_store/* && _outline_store*/); assert(_drawing); // todo: can minutely optimise this by only running the first part of on_idle() @@ -714,12 +731,12 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) // todo: remember the solid-colour optimisation to go into this // Blit from the backing store, without regard for the clean region. - cr->set_source(_backing_store, _store_rect.left() - _x0, _store_rect.top() - _y0); + cr->set_source(d->_backing_store, d->_store_rect.left() - _x0, d->_store_rect.top() - _y0); cr->paint(); // Paint unclean regions in red auto reg = Cairo::Region::create( Cairo::RectangleInt{ _x0, _y0, get_allocation().get_width(), get_allocation().get_height() } ); - reg->subtract(_clean_region); + reg->subtract(d->_clean_region); cr->set_source_rgba(1, 0, 0, 0.07); for (int i = 0; i < reg->get_num_rectangles(); i++) @@ -921,35 +938,35 @@ Canvas::on_idle() const auto canvas_rect = Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() ); // Assert that _clean_region is a subregion of _store_rect - auto tmp = _clean_region->copy(); - tmp->subtract(geom_to_cairo(_store_rect)); + auto tmp = d->_clean_region->copy(); + tmp->subtract(geom_to_cairo(d->_store_rect)); assert(tmp->empty()); // Ensure store contains canvas_rect const auto pad = Geom::IntPoint(200, 200); const auto device_scale = get_scale_factor(); - if (!_backing_store || _device_scale != device_scale || !_store_rect.intersects(canvas_rect)) + if (!d->_backing_store || d->_device_scale != device_scale || !d->_store_rect.intersects(canvas_rect)) { // Recreate the store, using the same memory if possible - _store_rect = Geom::IntRect::from_xywh( _x0, _y0, canvas_rect.width(), canvas_rect.height() ); - _store_rect.expandBy(pad); - _device_scale = device_scale; - if (!_backing_store || _backing_store->get_width() != _store_rect.width() * _device_scale || _backing_store->get_height() != _store_rect.height() * _device_scale) - _backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, _store_rect.width() * _device_scale, _store_rect.height() * _device_scale); - _clean_region = Cairo::Region::create(); + d->_store_rect = Geom::IntRect::from_xywh( _x0, _y0, canvas_rect.width(), canvas_rect.height() ); + d->_store_rect.expandBy(pad); + d->_device_scale = device_scale; + if (!d->_backing_store || d->_backing_store->get_width() != d->_store_rect.width() * d->_device_scale || d->_backing_store->get_height() != d->_store_rect.height() * d->_device_scale) + d->_backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, d->_store_rect.width() * d->_device_scale, d->_store_rect.height() * d->_device_scale); + d->_clean_region = Cairo::Region::create(); std::cout << "Recreated store" << std::endl; } - else if (!_store_rect.contains(canvas_rect)) + else if (!d->_store_rect.contains(canvas_rect)) { // Create new store, copy usable content across, set as new store auto store_rect = Geom::IntRect::from_xywh( _x0, _y0, canvas_rect.width(), canvas_rect.height() ); store_rect.expandBy(pad); - auto backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, store_rect.width() * _device_scale, store_rect.height() * _device_scale); + auto backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, store_rect.width() * d->_device_scale, store_rect.height() * d->_device_scale); - auto shift = store_rect.min() - _store_rect.min(); - auto reuse_rect = store_rect & _store_rect; + auto shift = store_rect.min() - d->_store_rect.min(); + auto reuse_rect = store_rect & d->_store_rect; auto cr = Cairo::Context::create(backing_store); // copy contents of store @@ -957,22 +974,22 @@ Canvas::on_idle() cr->save(); cr->rectangle(reuse_rect->left() - store_rect.left(), reuse_rect->top() - store_rect.top(), reuse_rect->width(), reuse_rect->height()); cr->clip(); - cr->set_source(_backing_store, -shift.x(), -shift.y()); + cr->set_source(d->_backing_store, -shift.x(), -shift.y()); cr->paint(); cr->restore(); - _store_rect = store_rect; - _backing_store = std::move(backing_store); - _clean_region->intersect(geom_to_cairo(_store_rect)); + d->_store_rect = store_rect; + d->_backing_store = std::move(backing_store); + d->_clean_region->intersect(geom_to_cairo(d->_store_rect)); std::cout << "Partially recreated store" << std::endl; } - assert(_store_rect.contains(canvas_rect)); + assert(d->_store_rect.contains(canvas_rect)); // Get region that requires painting auto region = Cairo::Region::create(geom_to_cairo(canvas_rect)); - region->subtract(_clean_region); + region->subtract(d->_clean_region); // Get mouse position in canvas space Geom::IntPoint mouse_loc; @@ -1113,18 +1130,18 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th _drawing->setRenderMode(_render_mode); _drawing->setColorMode(_color_mode); - paint_single_buffer(this_rect, setup.canvas_rect, _backing_store); + paint_single_buffer(this_rect, setup.canvas_rect, d->_backing_store); /*bool outline_overlay = _drawing->outlineOverlay(); if (_split_mode != Inkscape::SplitMode::NORMAL || outline_overlay) { _drawing->setRenderMode(Inkscape::RenderMode::OUTLINE); paint_single_buffer(this_rect, setup.canvas_rect, _outline_store); if (outline_overlay) { - _drawing->setRenderMode(Inkscape::RenderMode::OUTLINE_OVERLAY); + _drawing->setRenderMode(Inkscape::RenderMode::OUSplitTLINE_OVERLAY); } }*/ Cairo::RectangleInt crect = { this_rect.left(), this_rect.top(), this_rect.width(), this_rect.height() }; - _clean_region->do_union( crect ); + d->_clean_region->do_union( crect ); queue_draw_area(this_rect.left() - _x0, this_rect.top() - _y0, this_rect.width(), this_rect.height()); @@ -1197,14 +1214,14 @@ Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Geom::IntRect const // Make sure the following code does not go outside of store's data assert(store->get_format() == Cairo::FORMAT_ARGB32); - assert(_store_rect.contains(paint_rect)); + assert(d->_store_rect.contains(paint_rect)); /*auto cr = Cairo::Context::create(store); cr->set_source_rgba((rand() % 255) / 255.0, (rand() % 255) / 255.0, (rand() % 255) / 255.0, 0.2); cr->rectangle(paint_rect.left() - _store_rect.left(), paint_rect.top() - _store_rect.top(), paint_rect.width(), paint_rect.height()); cr->fill();*/ - Inkscape::CanvasItemBuffer buf(paint_rect, _store_rect, _device_scale); + Inkscape::CanvasItemBuffer buf(paint_rect, d->_store_rect, d->_device_scale); // Create temporary surface that draws directly to store. store->flush(); @@ -1225,20 +1242,20 @@ Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Geom::IntRect const double x_scale = 1.0; double y_scale = 1.0; cairo_surface_get_device_scale(store->cobj(), &x_scale, &y_scale); // No C++ API! - assert (_device_scale == (int) x_scale); - assert (_device_scale == (int) y_scale); + assert (d->_device_scale == (int) x_scale); + assert (d->_device_scale == (int) y_scale); // Move to the correct row. - data += stride * (paint_rect.top() - _store_rect.top()) * (int)y_scale; + data += stride * (paint_rect.top() - d->_store_rect.top()) * (int)y_scale; // Move to the correct column. - data += 4 * (paint_rect.left() - _store_rect.left()) * (int)x_scale; + data += 4 * (paint_rect.left() - d->_store_rect.left()) * (int)x_scale; auto imgs = Cairo::ImageSurface::create(data, Cairo::FORMAT_ARGB32, - paint_rect.width() * _device_scale, - paint_rect.height() * _device_scale, + paint_rect.width() * d->_device_scale, + paint_rect.height() * d->_device_scale, stride); - cairo_surface_set_device_scale(imgs->cobj(), _device_scale, _device_scale); // No C++ API! + cairo_surface_set_device_scale(imgs->cobj(), d->_device_scale, d->_device_scale); // No C++ API! auto cr = Cairo::Context::create(imgs); diff --git a/src/ui/widget/canvas.h b/src/ui/widget/canvas.h index 25c145e981..ca651eff8d 100644 --- a/src/ui/widget/canvas.h +++ b/src/ui/widget/canvas.h @@ -15,6 +15,8 @@ #include "config.h" #endif +#include + #include #include <2geom/rect.h> @@ -26,9 +28,6 @@ class SPDesktop; -struct PaintRectSetup; - - namespace Inkscape { class CanvasItem; @@ -38,6 +37,9 @@ class Drawing; namespace UI { namespace Widget { +class CanvasPrivate; +struct PaintRectSetup; + /** * A Gtk::DrawingArea widget for Inkscape's canvas. */ @@ -91,7 +93,7 @@ public: void set_cms_active(bool active) { _cms_active = active; } bool get_cms_active() { return _cms_active; } - Cairo::RefPtr get_backing_store() { return _backing_store; } // Background rotation preview + Cairo::RefPtr get_backing_store(); // Background rotation preview Cairo::RefPtr get_background_pattern() { return _background; } // For a GTK bug (see SelectedStyle::on_opacity_changed()). @@ -220,14 +222,6 @@ private: bool _in_destruction = false; // ======= CAIRO ======= ... Keep in one place - - /// Image surface storing a rendered part of the canvas - Cairo::RefPtr _backing_store; ///< Canvas content. - Cairo::RefPtr _outline_store; ///< Canvas outline content; only exists in split/x-ray mode. - Geom::IntRect _store_rect; ///< Rectangle of the store in world space. - Cairo::RefPtr _clean_region; ///< Subregion of store with up-to-date content. - int _device_scale = 1; ///< Scale for high DPI montiors. Probably should be double. - Cairo::RefPtr _background; ///< The background of the widget. // Used to update CanvasItemCtrl's when size changed. @@ -253,13 +247,16 @@ private: }; CanvasPrefObserver _size_observer; + + // Opaque pointer to implementation + friend class CanvasPrivate; + std::unique_ptr d; }; } // namespace Widget } // namespace UI } // namespace Inkscape - #endif // INKSCAPE_UI_WIDGET_CANVAS_H /* -- GitLab From daab5fab010fcf41420e13eb29f629535d29606b Mon Sep 17 00:00:00 2001 From: PBS Date: Mon, 10 Jan 2022 21:59:36 +0900 Subject: [PATCH 03/35] Fix the input event ordering problem. All major optisations now complete. Remaining tasks are 1. Visual niceties. 2. Fix fallout from hacks. 3. Remaining optimisations. --- src/ui/widget/canvas.cpp | 186 ++++++++++++++++++++++++++++----------- 1 file changed, 134 insertions(+), 52 deletions(-) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index e3a13aa3a9..f645c688ec 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -31,6 +31,8 @@ #include "display/control/canvas-item-group.h" #include "display/control/snap-indicator.h" +#include "../../../../frameclock/frameclock.h" + #include "ui/tools/tool-base.h" // Default cursor /* @@ -101,14 +103,90 @@ class CanvasPrivate private: friend class Canvas; + Canvas *q; Cairo::RefPtr _backing_store; ///< Canvas content. Cairo::RefPtr _outline_store; ///< Canvas outline content; only exists in split/x-ray mode. Geom::IntRect _store_rect; ///< Rectangle of the store in world space. Cairo::RefPtr _clean_region; ///< Subregion of store with up-to-date content. int _device_scale = 1; ///< Scale for high DPI montiors. Probably should be double. + + bool solid_background; + + sigc::connection hipri_idle; + sigc::connection lopri_idle; + + struct GdkEventFreer {void operator()(GdkEvent *ev) const {gdk_event_free(ev);}}; + std::vector> bucket; + sigc::connection bucket_emptier; + guint tickid = std::numeric_limits::max(); + + bool pending_draw = false; + + void schedule_bucket_emptier(); + void empty_bucket(); }; +void CanvasPrivate::schedule_bucket_emptier() +{ + std::cout << (bucket_emptier.connected() ? "already scheduled emptier" : "scheduling emptier") << std::endl; + + if (bucket_emptier.connected()) return; + + bucket_emptier = Glib::signal_idle().connect([this] + { + bucket_emptier.disconnect(); + empty_bucket(); + return false; + } + , G_PRIORITY_DEFAULT_IDLE - 5); // before lowpri_idle +} + +void CanvasPrivate::empty_bucket() +{ + auto f = Prof("bucket_emptier"); + std::cout << "emptying bucket" << std::endl; + + // todo: check if this hack can now be removed + if (q->_in_destruction) return; + + auto bucket2 = std::move(bucket); + + for (auto &event : bucket2) + { + // Block undo/redo while anything is dragged. + if (event->type == GDK_BUTTON_PRESS && event->button.button == 1) { + q->_is_dragging = true; + } else if (event->type == GDK_BUTTON_RELEASE) { + q->_is_dragging = false; + } + + bool finished = false; // Can't be bool or "stack smashing detected"! + + if (q->_current_canvas_item) { + // Choose where to send event; + CanvasItem *item = q->_current_canvas_item; + + if (q->_grabbed_canvas_item && !q->_current_canvas_item->is_descendant_of(q->_grabbed_canvas_item)) { + item = q->_grabbed_canvas_item; + } + + // Propagate the event up the canvas item hierarchy until handled. + while (item) { + finished = item->handle_event(event.get()); + if (finished) break; + item = item->get_parent(); + } + } + + if (!finished) + { + // todo: should really re-fire this event, because it wants to be handled somewhere else + // - but I've not observbed any problems from discarding it yet + } + } +} + Canvas::Canvas() : _size_observer(this, "/options/grabsize/value"), d(std::make_unique()) { @@ -135,12 +213,15 @@ Canvas::Canvas() d->_clean_region = Cairo::Region::create(); _background = Cairo::SolidPattern::create_rgb(1.0, 1.0, 1.0); + d->solid_background = true; _canvas_item_root = new Inkscape::CanvasItemGroup(nullptr); _canvas_item_root->set_name("CanvasItemGroup:Root"); _canvas_item_root->set_canvas(this); srand(g_get_monotonic_time()); + + d->q = this; } Canvas::~Canvas() @@ -318,6 +399,7 @@ Canvas::set_background_color(guint32 rgba) double b = SP_RGBA32_B_F(rgba); _background = Cairo::SolidPattern::create_rgb(r, g, b); + d->solid_background = true; redraw_all(); } @@ -330,6 +412,7 @@ Canvas::set_background_checkerboard(guint32 rgba) { auto pattern = ink_cairo_pattern_create_checkerboard(rgba); _background = Cairo::RefPtr(new Cairo::Pattern(pattern)); + d->solid_background = false; redraw_all(); } @@ -711,28 +794,34 @@ Canvas::on_motion_notify_event(GdkEventMotion *motion_event) bool Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) { + Prof f; + // sp_canvas_item_recursive_print_tree(0, _root); // canvas_item_print_tree(_canvas_item_root); - std::cout << "on_draw\n"; + // todo: can minutely optimise this by only running the first part of on_idle() assert(d->_backing_store/* && _outline_store*/); assert(_drawing); - // todo: can minutely optimise this by only running the first part of on_idle() - on_idle(); - // Blit background (e.g. checkerboard). - cr->save(); - cr->set_operator(Cairo::OPERATOR_SOURCE); - cr->set_source(_background); - cr->paint(); - cr->restore(); - // todo: remember the solid-colour optimisation to go into this + if (!d->solid_background) + { + f = Prof("background"); + cr->save(); + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->set_source(_background); + cr->paint(); + cr->restore(); + } // Blit from the backing store, without regard for the clean region. + f = Prof("draw"); + cr->save(); + if (d->solid_background) cr->set_operator(Cairo::OPERATOR_SOURCE); cr->set_source(d->_backing_store, d->_store_rect.left() - _x0, d->_store_rect.top() - _y0); cr->paint(); + cr->restore(); // Paint unclean regions in red auto reg = Cairo::Region::create( Cairo::RectangleInt{ _x0, _y0, get_allocation().get_width(), get_allocation().get_height() } ); @@ -746,6 +835,13 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->fill(); } + if (d->pending_draw) + { + std::cout << "finishing draw, events were not allowed but now are" << std::endl; + if (!d->bucket.empty()) d->schedule_bucket_emptier(); + d->pending_draw = false; + } + /* // Draw overlay if required. if (_drawing->outlineOverlay()) { @@ -877,22 +973,19 @@ Canvas::update_canvas_item_ctrl_sizes(int size_index) void Canvas::add_idle() { + auto f = FuncProf(); + if (_in_destruction) { std::cerr << "Canvas::add_idle: Called after canvas destroyed!" << std::endl; return; } - if (!_idle_connection.connected()) { - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - guint redrawPriority = prefs->getIntLimited("/options/redrawpriority/value", G_PRIORITY_HIGH_IDLE, G_PRIORITY_HIGH_IDLE, G_PRIORITY_DEFAULT_IDLE); - if (_in_full_redraw) { - _in_full_redraw = false; - redrawPriority = G_PRIORITY_DEFAULT_IDLE; - } - // G_PRIORITY_HIGH_IDLE = 100, G_PRIORITY_DEFAULT_IDLE = 200: Higher number => lower priority. + if (!d->hipri_idle.connected()) { + d->hipri_idle = Glib::signal_idle().connect([this] {on_idle(); return false;}, G_PRIORITY_HIGH_IDLE + 15); // after resize, before draw + } - // TEMP HACK: the high priority idle screws with my redraw implementation, so ignoring it for now - _idle_connection = Glib::signal_idle().connect(sigc::mem_fun(*this, &Canvas::on_idle), G_PRIORITY_DEFAULT_IDLE); + if (!d->lopri_idle.connected()) { + d->lopri_idle = Glib::signal_idle().connect(sigc::mem_fun(*this, &Canvas::on_idle), G_PRIORITY_DEFAULT_IDLE); } } @@ -918,6 +1011,8 @@ distSq(const Geom::IntPoint pt, const Geom::IntRect &rect) bool Canvas::on_idle() { + auto f = FuncProf(); + if (_in_destruction) { std::cerr << "Canvas::on_idle: Called after canvas destroyed!" << std::endl; } @@ -1043,25 +1138,30 @@ Canvas::on_idle() // Timed out. Temporarily return to idle loop, and come back here if still idle. std::cout << "timed out: " << g_get_monotonic_time() - setup.start_time << " us \n"; _forced_redraw_count++; + f.subtype = 1; return true; } } - // Check if suppressed a time out, and adjust counter if so + // Check if suppressed a timeout, and adjust counter if so if (setup.disable_timeouts) { auto now = g_get_monotonic_time(); auto elapsed = now - setup.start_time; if (elapsed > 1000) { // Timed out - std::cout << "ignore timeout: " << g_get_monotonic_time() - setup.start_time << " us \n"; + std::cout << "ignored timeout: " << g_get_monotonic_time() - setup.start_time << " us \n"; _forced_redraw_count = 0; + + f.subtype = 2; + return false; } } // todo: check clean region is what it should be std::cout << "finished drawing\n"; + f.subtype = 3; return false; } @@ -1144,6 +1244,11 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th d->_clean_region->do_union( crect ); queue_draw_area(this_rect.left() - _x0, this_rect.top() - _y0, this_rect.width(), this_rect.height()); + if (!d->pending_draw) + { + std::cout << "marking pending redraw" << std::endl; + d->pending_draw = true; + } return true; } @@ -1245,7 +1350,6 @@ Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Geom::IntRect const assert (d->_device_scale == (int) x_scale); assert (d->_device_scale == (int) y_scale); - // Move to the correct row. data += stride * (paint_rect.top() - d->_store_rect.top()) * (int)y_scale; // Move to the correct column. @@ -1262,7 +1366,7 @@ Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Geom::IntRect const // Clear background cr->save(); cr->set_operator(Cairo::OPERATOR_SOURCE); - cr->set_source_rgba(0,0,0,0); + if (!d->solid_background) cr->set_source_rgba(0,0,0,0); else cr->set_source(_background); cr->paint(); cr->restore(); @@ -1561,6 +1665,8 @@ Canvas::pick_current_item(GdkEvent *event) bool Canvas::emit_event(GdkEvent *event) { + auto f = FuncProf(); + Gdk::EventMask mask = (Gdk::EventMask)0; if (_grabbed_canvas_item) { switch (event->type) { @@ -1621,37 +1727,13 @@ Canvas::emit_event(GdkEvent *event) break; } - // Block undo/redo while anything is dragged. - if (event->type == GDK_BUTTON_PRESS && event->button.button == 1) { - _is_dragging = true; - } else if (event->type == GDK_BUTTON_RELEASE) { - _is_dragging = false; - } + std::cout << "adding event to bucket" << std::endl; + d->bucket.emplace_back(event_copy); + if (!d->pending_draw) {std::cout << "scheduling tick callback" << std::endl; add_tick_callback([this] (const Glib::RefPtr&) {d->schedule_bucket_emptier(); return false;});} - gint finished = false; // Can't be bool or "stack smashing detected"! - - if (_current_canvas_item) { - // Choose where to send event; - CanvasItem *item = _current_canvas_item; - - if (_grabbed_canvas_item && !_current_canvas_item->is_descendant_of(_grabbed_canvas_item)) { - item = _grabbed_canvas_item; - } - - // Propagate the event up the canvas item hierarchy until handled. - while (item) { - finished = item->handle_event(event_copy); - if (finished) break; - item = item->get_parent(); - } - } - - gdk_event_free(event_copy); - - return finished; + return true; } - } // namespace Widget } // namespace UI } // namespace Inkscape -- GitLab From af946dcd9090ef98dcf15d619f89e7a07c53967a Mon Sep 17 00:00:00 2001 From: PBS Date: Tue, 11 Jan 2022 03:33:19 +0900 Subject: [PATCH 04/35] Replace event discarding hack with event forwarding hack Earlier I just discarded events as a deliberate hack. Having not found an easy better solution, I "fixed" this hack with an even worse hack that forwards them back to the parent window, then when they come back to the canvas again, ignores them. This at least gets input working properly again. However: This has the potential to blow up spectacularly in the future when you are least expecting it. Please fix before then! --- src/ui/widget/canvas.cpp | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index f645c688ec..3f63733b4c 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -125,11 +125,13 @@ private: void schedule_bucket_emptier(); void empty_bucket(); + + GdkEvent *ignore = nullptr; }; void CanvasPrivate::schedule_bucket_emptier() { - std::cout << (bucket_emptier.connected() ? "already scheduled emptier" : "scheduling emptier") << std::endl; + //std::cout << (bucket_emptier.connected() ? "already scheduled emptier" : "scheduling emptier") << std::endl; if (bucket_emptier.connected()) return; @@ -145,7 +147,7 @@ void CanvasPrivate::schedule_bucket_emptier() void CanvasPrivate::empty_bucket() { auto f = Prof("bucket_emptier"); - std::cout << "emptying bucket" << std::endl; + //std::cout << "emptying bucket" << std::endl; // todo: check if this hack can now be removed if (q->_in_destruction) return; @@ -181,8 +183,10 @@ void CanvasPrivate::empty_bucket() if (!finished) { - // todo: should really re-fire this event, because it wants to be handled somewhere else - // - but I've not observbed any problems from discarding it yet + // UNACCEPTABLE HACK!!! BUT I HAVE NO CHOICE + ignore = event.get(); + q->get_toplevel()->event(event.get()); + ignore = nullptr; } } } @@ -288,7 +292,6 @@ Canvas::redraw_all() // We need to ignore their requests! return; } - _in_full_redraw = true; d->_clean_region = Cairo::Region::create(); // Empty region (i.e. everything is dirty). add_idle(); } @@ -837,7 +840,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) if (d->pending_draw) { - std::cout << "finishing draw, events were not allowed but now are" << std::endl; + //std::cout << "finishing draw, events were not allowed but now are" << std::endl; if (!d->bucket.empty()) d->schedule_bucket_emptier(); d->pending_draw = false; } @@ -1160,7 +1163,7 @@ Canvas::on_idle() // todo: check clean region is what it should be - std::cout << "finished drawing\n"; + //std::cout << "finished drawing\n"; f.subtype = 3; return false; } @@ -1246,7 +1249,7 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th queue_draw_area(this_rect.left() - _x0, this_rect.top() - _y0, this_rect.width(), this_rect.height()); if (!d->pending_draw) { - std::cout << "marking pending redraw" << std::endl; + //std::cout << "marking pending redraw" << std::endl; d->pending_draw = true; } @@ -1667,6 +1670,8 @@ Canvas::emit_event(GdkEvent *event) { auto f = FuncProf(); + if (event == d->ignore) return false; + Gdk::EventMask mask = (Gdk::EventMask)0; if (_grabbed_canvas_item) { switch (event->type) { @@ -1727,9 +1732,9 @@ Canvas::emit_event(GdkEvent *event) break; } - std::cout << "adding event to bucket" << std::endl; + //std::cout << "adding event to bucket" << std::endl; d->bucket.emplace_back(event_copy); - if (!d->pending_draw) {std::cout << "scheduling tick callback" << std::endl; add_tick_callback([this] (const Glib::RefPtr&) {d->schedule_bucket_emptier(); return false;});} + if (!d->pending_draw) {/*std::cout << "scheduling tick callback" << std::endl;*/ add_tick_callback([this] (const Glib::RefPtr&) {d->schedule_bucket_emptier(); return false;});} return true; } -- GitLab From 948a166f66a8771e717998ec84df528eeac26076 Mon Sep 17 00:00:00 2001 From: PBS Date: Tue, 11 Jan 2022 03:48:14 +0900 Subject: [PATCH 05/35] Cleanup of the worst cruft --- src/ui/widget/canvas.cpp | 51 ++++------------------------------------ 1 file changed, 4 insertions(+), 47 deletions(-) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 3f63733b4c..3c84f8dc08 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -131,8 +131,6 @@ private: void CanvasPrivate::schedule_bucket_emptier() { - //std::cout << (bucket_emptier.connected() ? "already scheduled emptier" : "scheduling emptier") << std::endl; - if (bucket_emptier.connected()) return; bucket_emptier = Glib::signal_idle().connect([this] @@ -147,10 +145,6 @@ void CanvasPrivate::schedule_bucket_emptier() void CanvasPrivate::empty_bucket() { auto f = Prof("bucket_emptier"); - //std::cout << "emptying bucket" << std::endl; - - // todo: check if this hack can now be removed - if (q->_in_destruction) return; auto bucket2 = std::move(bucket); @@ -163,10 +157,10 @@ void CanvasPrivate::empty_bucket() q->_is_dragging = false; } - bool finished = false; // Can't be bool or "stack smashing detected"! + bool finished = false; if (q->_current_canvas_item) { - // Choose where to send event; + // Choose where to send event CanvasItem *item = q->_current_canvas_item; if (q->_grabbed_canvas_item && !q->_current_canvas_item->is_descendant_of(q->_grabbed_canvas_item)) { @@ -302,11 +296,6 @@ Canvas::redraw_all() void Canvas::redraw_area(int x0, int y0, int x1, int y1) { - // std::cout << "Canvas::redraw_area: " - // << " x0: " << x0 - // << " y0: " << y0 - // << " x1: " << x1 - // << " y1: " << y1 << std::endl; if (_in_destruction) { // CanvasItems redraw their area when being deleted... which happens when the Canvas is destroyed. // We need to ignore their requests! @@ -840,7 +829,6 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) if (d->pending_draw) { - //std::cout << "finishing draw, events were not allowed but now are" << std::endl; if (!d->bucket.empty()) d->schedule_bucket_emptier(); d->pending_draw = false; } @@ -1163,7 +1151,6 @@ Canvas::on_idle() // todo: check clean region is what it should be - //std::cout << "finished drawing\n"; f.subtype = 3; return false; } @@ -1247,11 +1234,7 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th d->_clean_region->do_union( crect ); queue_draw_area(this_rect.left() - _x0, this_rect.top() - _y0, this_rect.width(), this_rect.height()); - if (!d->pending_draw) - { - //std::cout << "marking pending redraw" << std::endl; - d->pending_draw = true; - } + d->pending_draw = true; return true; } @@ -1334,14 +1317,6 @@ Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Geom::IntRect const // Create temporary surface that draws directly to store. store->flush(); - // std::cout << " Writing store to png" << std::endl; - // static int i = 0; - // ++i; - // if (i < 5) { - // std::string file = "paint_single_buffer0_" + std::to_string(i) + ".png"; - // store->write_to_png(file); - // } - // Create temporary surface that draws directly to store. unsigned char *data = store->get_data(); int stride = store->get_stride(); @@ -1403,23 +1378,6 @@ Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Geom::IntRect const } store->mark_dirty(); - - // if (i < 5) { - // std::cout << " Writing store to png" << std::endl; - // std::string file = "paint_single_buffer1_" + std::to_string(i) + ".png"; - // store->write_to_png(file); - // } - - // Uncomment to see how Inkscape paints to rectangles on canvas. - // cr->save(); - // cr->move_to (0.5, 0.5); - // cr->line_to (0.5, paint_rect.height()-0.5); - // cr->line_to (paint_rect.width()-0.5, paint_rect.height()-0.5); - // cr->line_to (paint_rect.width()-0.5, 0.5); - // cr->close_path(); - // cr->set_source_rgba(0.0, 0.0, 0.5, 1.0); - // cr->stroke(); - // cr->restore(); } // Sets clip path for Split and X-Ray modes. @@ -1732,9 +1690,8 @@ Canvas::emit_event(GdkEvent *event) break; } - //std::cout << "adding event to bucket" << std::endl; d->bucket.emplace_back(event_copy); - if (!d->pending_draw) {/*std::cout << "scheduling tick callback" << std::endl;*/ add_tick_callback([this] (const Glib::RefPtr&) {d->schedule_bucket_emptier(); return false;});} + if (!d->pending_draw) add_tick_callback([this] (const Glib::RefPtr&) {d->schedule_bucket_emptier(); return false;}); return true; } -- GitLab From c5c181781dae9a92a6f60a6cead730dac28148ab Mon Sep 17 00:00:00 2001 From: PBS Date: Tue, 11 Jan 2022 22:17:51 +0900 Subject: [PATCH 06/35] Cleaning up, create debug switches. --- src/ui/widget/canvas.cpp | 246 +++++++++++++++++++++++++-------------- 1 file changed, 157 insertions(+), 89 deletions(-) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 3c84f8dc08..6fa5c8b938 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -12,7 +12,7 @@ */ #include -#include +#include // Sort #include @@ -26,15 +26,44 @@ #include "desktop.h" #include "preferences.h" -#include "display/cairo-utils.h" // Checkerboard background. +#include "display/cairo-utils.h" // Checkerboard background #include "display/drawing.h" #include "display/control/canvas-item-group.h" #include "display/control/snap-indicator.h" -#include "../../../../frameclock/frameclock.h" - #include "ui/tools/tool-base.h" // Default cursor +// Debugging switches +#define ENABLE_FRAMECHECK 0 +#define ENABLE_LOGGING 1 +#define ENABLE_SLOW_REDRAW 1 +#define ENABLE_SHOW_REDRAW 0 +#define ENABLE_SHOW_UNCLEAN 1 + +#if ENABLE_FRAMECHECK +#include "../../../../framecheck.h" +#define IF_FRAMECHECK(X) X +#else +#define IF_FRAMECHECK(X) +#endif + +#if ENABLE_LOGGING +#define IF_LOGGING(X) X +#else +#define IF_LOGGING(X) +#endif + +#if ENABLE_SLOW_REDRAW +#define SLOW_REDRAW_SIZE 30 // px +#define SLOW_REDRAW_TIME 50 // us +#endif + +#if ENABLE_SHOW_UNCLEAN +#define IF_SHOW_UNCLEAN(X) X +#else +#define IF_SHOW_UNCLEAN(X) +#endif + /* * The canvas is responsible for rendering the SVG drawing with various "control" * items below and on top of the drawing. Rendering is triggered by a call to one of: @@ -110,6 +139,7 @@ private: Geom::IntRect _store_rect; ///< Rectangle of the store in world space. Cairo::RefPtr _clean_region; ///< Subregion of store with up-to-date content. int _device_scale = 1; ///< Scale for high DPI montiors. Probably should be double. + bool _store_solid_background; bool solid_background; @@ -119,7 +149,6 @@ private: struct GdkEventFreer {void operator()(GdkEvent *ev) const {gdk_event_free(ev);}}; std::vector> bucket; sigc::connection bucket_emptier; - guint tickid = std::numeric_limits::max(); bool pending_draw = false; @@ -144,7 +173,7 @@ void CanvasPrivate::schedule_bucket_emptier() void CanvasPrivate::empty_bucket() { - auto f = Prof("bucket_emptier"); + IF_FRAMECHECK(framecheck_whole_function) auto bucket2 = std::move(bucket); @@ -287,6 +316,7 @@ Canvas::redraw_all() return; } d->_clean_region = Cairo::Region::create(); // Empty region (i.e. everything is dirty). + IF_SHOW_UNCLEAN( queue_draw() ); add_idle(); } @@ -319,6 +349,7 @@ Canvas::redraw_area(int x0, int y0, int x1, int y1) Cairo::RectangleInt crect = { x0, y0, x1-x0, y1-y0 }; d->_clean_region->subtract(crect); + IF_SHOW_UNCLEAN( queue_draw() ); add_idle(); } @@ -494,7 +525,7 @@ void Canvas::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const { minimum_width = natural_width = 256; -} +} void Canvas::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const @@ -767,6 +798,23 @@ Canvas::on_motion_notify_event(GdkEventMotion *motion_event) return status; } +namespace { +auto geom_to_cairo(Geom::Affine affine) +{ + return Cairo::Matrix(affine[0], affine[1], affine[2], affine[3], affine[4], affine[5]); +} +} + +void +fill_cairo_region(const Cairo::RefPtr &cr, const Cairo::RefPtr ®) +{ + for (int i = 0; i < reg->get_num_rectangles(); i++) { + auto rect = reg->get_rectangle(i); + cr->rectangle(rect.x, rect.y, rect.width, rect.height); + cr->fill(); + } +} + /* * The on_draw() function is called whenever Gtk wants to update the window. This function: * @@ -786,20 +834,18 @@ Canvas::on_motion_notify_event(GdkEventMotion *motion_event) bool Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) { - Prof f; + IF_FRAMECHECK( Prof f; ) // sp_canvas_item_recursive_print_tree(0, _root); // canvas_item_print_tree(_canvas_item_root); - // todo: can minutely optimise this by only running the first part of on_idle() - assert(d->_backing_store/* && _outline_store*/); assert(_drawing); // Blit background (e.g. checkerboard). if (!d->solid_background) { - f = Prof("background"); + IF_FRAMECHECK( f = Prof("background"); ) cr->save(); cr->set_operator(Cairo::OPERATOR_SOURCE); cr->set_source(_background); @@ -807,25 +853,25 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->restore(); } - // Blit from the backing store, without regard for the clean region. - f = Prof("draw"); + // Blit backing store to screen. + IF_FRAMECHECK( f = Prof("draw"); ) cr->save(); - if (d->solid_background) cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->set_operator(d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); cr->set_source(d->_backing_store, d->_store_rect.left() - _x0, d->_store_rect.top() - _y0); cr->paint(); cr->restore(); - // Paint unclean regions in red + #if ENABLE_SHOW_UNCLEAN + IF_FRAMECHECK( f = Prof("paint_unclean"); ) + // Paint unclean regions in red. auto reg = Cairo::Region::create( Cairo::RectangleInt{ _x0, _y0, get_allocation().get_width(), get_allocation().get_height() } ); reg->subtract(d->_clean_region); - - cr->set_source_rgba(1, 0, 0, 0.07); - for (int i = 0; i < reg->get_num_rectangles(); i++) - { - auto rect = reg->get_rectangle(i); - cr->rectangle(rect.x - _x0, rect.y - _y0, rect.width, rect.height); - cr->fill(); - } + reg->translate(-_x0, -_y0); + cr->save(); + cr->set_source_rgba(1, 0, 0, 0.2); + fill_cairo_region(cr, reg); + cr->restore(); + #endif if (d->pending_draw) { @@ -947,7 +993,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) } dirty_region->subtract(_clean_region); - + if (!dirty_region->empty()) { add_idle(); }*/ @@ -964,7 +1010,7 @@ Canvas::update_canvas_item_ctrl_sizes(int size_index) void Canvas::add_idle() { - auto f = FuncProf(); + IF_FRAMECHECK(framecheck_whole_function) if (_in_destruction) { std::cerr << "Canvas::add_idle: Called after canvas destroyed!" << std::endl; @@ -1002,7 +1048,7 @@ distSq(const Geom::IntPoint pt, const Geom::IntRect &rect) bool Canvas::on_idle() { - auto f = FuncProf(); + IF_FRAMECHECK( auto f = FuncProf(); ) if (_in_destruction) { std::cerr << "Canvas::on_idle: Called after canvas destroyed!" << std::endl; @@ -1032,17 +1078,27 @@ Canvas::on_idle() const auto pad = Geom::IntPoint(200, 200); const auto device_scale = get_scale_factor(); - if (!d->_backing_store || d->_device_scale != device_scale || !d->_store_rect.intersects(canvas_rect)) + if (!d->_backing_store || d->_device_scale != device_scale || d->_store_solid_background != d->solid_background || !d->_store_rect.intersects(canvas_rect)) { // Recreate the store, using the same memory if possible d->_store_rect = Geom::IntRect::from_xywh( _x0, _y0, canvas_rect.width(), canvas_rect.height() ); d->_store_rect.expandBy(pad); d->_device_scale = device_scale; - if (!d->_backing_store || d->_backing_store->get_width() != d->_store_rect.width() * d->_device_scale || d->_backing_store->get_height() != d->_store_rect.height() * d->_device_scale) - d->_backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, d->_store_rect.width() * d->_device_scale, d->_store_rect.height() * d->_device_scale); - d->_clean_region = Cairo::Region::create(); + d->_store_solid_background = d->solid_background; + int desired_width = d->_store_rect.width() * d->_device_scale; + int desired_height = d->_store_rect.height() * d->_device_scale; + if (!d->_backing_store || d->_backing_store->get_width() != desired_width || d->_backing_store->get_height() != desired_height) + d->_backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, desired_width, desired_height); + if (d->solid_background) { + auto cr = Cairo::Context::create(d->_backing_store); + cr->set_source(_background); + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->paint(); + } + d->_clean_region = Cairo::Region::create(); + IF_SHOW_UNCLEAN( queue_draw() ); - std::cout << "Recreated store" << std::endl; + IF_LOGGING( std::cout << "Recreated store" << std::endl; ) } else if (!d->_store_rect.contains(canvas_rect)) { @@ -1053,22 +1109,36 @@ Canvas::on_idle() auto shift = store_rect.min() - d->_store_rect.min(); auto reuse_rect = store_rect & d->_store_rect; + assert(reuse_rect); auto cr = Cairo::Context::create(backing_store); - // copy contents of store - assert(reuse_rect); + // Paint background where necessary + if (d->solid_background) { + auto reg = Cairo::Region::create(geom_to_cairo(store_rect)); + reg->subtract(geom_to_cairo(*reuse_rect)); + reg->translate(-store_rect.left(), -store_rect.top()); + cr->save(); + cr->set_source(_background); + cr->set_operator(Cairo::OPERATOR_SOURCE); + fill_cairo_region(cr, reg); + cr->restore(); + } + + // Copy usuable contents of store shifted cr->save(); cr->rectangle(reuse_rect->left() - store_rect.left(), reuse_rect->top() - store_rect.top(), reuse_rect->width(), reuse_rect->height()); cr->clip(); cr->set_source(d->_backing_store, -shift.x(), -shift.y()); + cr->set_operator(Cairo::OPERATOR_SOURCE); cr->paint(); cr->restore(); d->_store_rect = store_rect; d->_backing_store = std::move(backing_store); d->_clean_region->intersect(geom_to_cairo(d->_store_rect)); + IF_SHOW_UNCLEAN( queue_draw() ); - std::cout << "Partially recreated store" << std::endl; + IF_LOGGING( std::cout << "Shifted store" << std::endl; ) } assert(d->_store_rect.contains(canvas_rect)); @@ -1087,7 +1157,7 @@ Canvas::on_idle() mouse_loc = Geom::IntPoint(_x0 + x, _y0 + y); } else { - mouse_loc = canvas_rect.midpoint(); + mouse_loc = canvas_rect.midpoint(); } // Obtain rectangles list sorted by distance from mouse @@ -1127,9 +1197,9 @@ Canvas::on_idle() if (!paint_rect_internal(setup, rect)) { // Timed out. Temporarily return to idle loop, and come back here if still idle. - std::cout << "timed out: " << g_get_monotonic_time() - setup.start_time << " us \n"; + IF_LOGGING( std::cout << "Timed out: " << g_get_monotonic_time() - setup.start_time << " us \n"; ) + IF_FRAMECHECK( f.subtype = 1; ) _forced_redraw_count++; - f.subtype = 1; return true; } } @@ -1141,17 +1211,17 @@ Canvas::on_idle() auto elapsed = now - setup.start_time; if (elapsed > 1000) { // Timed out - std::cout << "ignored timeout: " << g_get_monotonic_time() - setup.start_time << " us \n"; + IF_LOGGING( std::cout << "Ignored timeout: " << g_get_monotonic_time() - setup.start_time << " us \n"; ) _forced_redraw_count = 0; - f.subtype = 2; + IF_FRAMECHECK( f.subtype = 2; ) return false; } } - // todo: check clean region is what it should be + // todo: check clean region is what it should be (should have finished drawing by now) - f.subtype = 3; + IF_FRAMECHECK( f.subtype = 3; ) return false; } @@ -1162,7 +1232,7 @@ Canvas::on_idle() bool Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &this_rect) { - // Find optimal buffer dimension + // Find optimal rectangle dimension int bw = this_rect.width(); int bh = this_rect.height(); @@ -1171,9 +1241,9 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th return true; } - /* - // Uncomment for artificial redraw slowness fun - you won't regret it! - if (bw > bh || bw > 10) { + #if ENABLE_SLOW_REDRAW + // Aggressively subdivide into many small rectangles + if (bw > bh || bw > SLOW_REDRAW_SIZE) { int mid = this_rect[Geom::X].middle(); Geom::IntRect lo, hi; @@ -1181,14 +1251,13 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th hi = Geom::IntRect(mid, this_rect.top(), this_rect.right(), this_rect.bottom()); if (setup.mouse_loc[Geom::X] < mid) { - // Always paint towards the mouse first return paint_rect_internal(setup, lo) && paint_rect_internal(setup, hi); } else { return paint_rect_internal(setup, hi) && paint_rect_internal(setup, lo); } - } else if (bh > bw && bh > 10) { + } else if (bh > bw && bh > SLOW_REDRAW_SIZE) { int mid = this_rect[Geom::Y].middle(); Geom::IntRect lo, hi; @@ -1196,7 +1265,6 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th hi = Geom::IntRect(this_rect.left(), mid, this_rect.right(), this_rect.bottom()); if (setup.mouse_loc[Geom::Y] < mid) { - // Always paint towards the mouse first return paint_rect_internal(setup, lo) && paint_rect_internal(setup, hi); } else { @@ -1204,23 +1272,15 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th && paint_rect_internal(setup, lo); } } - */ + #endif // ENABLE_SLOW_REDRAW if (bw * bh < setup.max_pixels) { - // We are small enough! - - if (!setup.disable_timeouts) { - auto now = g_get_monotonic_time(); - auto elapsed = now - setup.start_time; - if (elapsed > 1000) { - return false; - } - } + // Rectangle is small enough _drawing->setRenderMode(_render_mode); _drawing->setColorMode(_color_mode); - paint_single_buffer(this_rect, setup.canvas_rect, d->_backing_store); + /*bool outline_overlay = _drawing->outlineOverlay(); if (_split_mode != Inkscape::SplitMode::NORMAL || outline_overlay) { _drawing->setRenderMode(Inkscape::RenderMode::OUTLINE); @@ -1230,12 +1290,24 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th } }*/ + #if ENABLE_SLOW_REDRAW + usleep(SLOW_REDRAW_TIME); // Introduce an artificial delay for each rectangle + #endif + Cairo::RectangleInt crect = { this_rect.left(), this_rect.top(), this_rect.width(), this_rect.height() }; d->_clean_region->do_union( crect ); queue_draw_area(this_rect.left() - _x0, this_rect.top() - _y0, this_rect.width(), this_rect.height()); d->pending_draw = true; + if (!setup.disable_timeouts) { + auto now = g_get_monotonic_time(); + auto elapsed = now - setup.start_time; + if (elapsed > 1000) { + return false; + } + } + return true; } @@ -1297,27 +1369,13 @@ void Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Geom::IntRect const &canvas_rect, Cairo::RefPtr &store) { - if (!store) { - std::cerr << "Canvas::paint_single_buffer: store not created!" << std::endl; - return; - // Maybe store not created! - todo: check if this can actually happen - } - // Make sure the following code does not go outside of store's data + assert(store); assert(store->get_format() == Cairo::FORMAT_ARGB32); assert(d->_store_rect.contains(paint_rect)); - /*auto cr = Cairo::Context::create(store); - cr->set_source_rgba((rand() % 255) / 255.0, (rand() % 255) / 255.0, (rand() % 255) / 255.0, 0.2); - cr->rectangle(paint_rect.left() - _store_rect.left(), paint_rect.top() - _store_rect.top(), paint_rect.width(), paint_rect.height()); - cr->fill();*/ - - Inkscape::CanvasItemBuffer buf(paint_rect, d->_store_rect, d->_device_scale); - // Create temporary surface that draws directly to store. store->flush(); - - // Create temporary surface that draws directly to store. unsigned char *data = store->get_data(); int stride = store->get_stride(); @@ -1343,34 +1401,44 @@ Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Geom::IntRect const // Clear background cr->save(); - cr->set_operator(Cairo::OPERATOR_SOURCE); - if (!d->solid_background) cr->set_source_rgba(0,0,0,0); else cr->set_source(_background); + if (d->solid_background) { + cr->set_source(_background); + cr->set_operator(Cairo::OPERATOR_SOURCE); + } + else { + cr->set_operator(Cairo::OPERATOR_CLEAR); + } cr->paint(); cr->restore(); - buf.cr = cr; - // Render drawing on top of background. if (_canvas_item_root->is_visible()) { + Inkscape::CanvasItemBuffer buf(paint_rect, d->_store_rect, d->_device_scale); + buf.cr = cr; _canvas_item_root->render(&buf); } + #if ENABLE_SHOW_REDRAW + // Paint over newly drawn content with a translucent random colour + cr->set_source_rgba((rand() % 255) / 255.0, (rand() % 255) / 255.0, (rand() % 255) / 255.0, 0.2); + cr->set_operator(Cairo::OPERATOR_OVER); + cr->rectangle(0, 0, imgs->get_width(), imgs->get_height()); + cr->fill(); + #endif + if (_cms_active) { - cmsHTRANSFORM transf = nullptr; Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - bool fromDisplay = prefs->getBool( "/options/displayprofile/from_display"); - if ( fromDisplay ) { - transf = Inkscape::CMSSystem::getDisplayPer(_cms_key); - } else { - transf = Inkscape::CMSSystem::getDisplayTransform(); - } + + auto transf = prefs->getBool("/options/displayprofile/from_display") + ? Inkscape::CMSSystem::getDisplayPer(_cms_key) + : Inkscape::CMSSystem::getDisplayTransform(); if (transf) { imgs->flush(); unsigned char *px = imgs->get_data(); int stride = imgs->get_stride(); - for (int i=0; imark_dirty(); @@ -1496,7 +1564,7 @@ Canvas::pick_current_item(GdkEvent *event) GDK_BUTTON5_MASK); if (!button_down) _left_grabbed_item = false; } - + // Save the event in the canvas. This is used to synthesize enter and // leave events in case the current item changes. It is also used to // re-pick the current item if the current one gets deleted. Also, @@ -1626,7 +1694,7 @@ Canvas::pick_current_item(GdkEvent *event) bool Canvas::emit_event(GdkEvent *event) { - auto f = FuncProf(); + IF_FRAMECHECK(framecheck_whole_function) if (event == d->ignore) return false; -- GitLab From bdeba7ae2ac256575829b9d47f3cbba41880ddf6 Mon Sep 17 00:00:00 2001 From: PBS Date: Wed, 12 Jan 2022 23:49:01 +0900 Subject: [PATCH 07/35] Decoupled mode --- src/display/control/canvas-item-buffer.h | 15 +- src/object/sp-namedview.cpp | 3 - src/ui/widget/canvas.cpp | 316 ++++++++++++++--------- src/ui/widget/canvas.h | 38 ++- src/widgets/desktop-widget.cpp | 1 - 5 files changed, 217 insertions(+), 156 deletions(-) diff --git a/src/display/control/canvas-item-buffer.h b/src/display/control/canvas-item-buffer.h index 0215b51753..5bc5846edc 100644 --- a/src/display/control/canvas-item-buffer.h +++ b/src/display/control/canvas-item-buffer.h @@ -26,23 +26,10 @@ namespace Inkscape { /** * Class used when rendering canvas items. */ -class CanvasItemBuffer { -public: - CanvasItemBuffer(Geom::IntRect const &paint_rect, Geom::IntRect const &canvas_rect, int device_scale) - : rect(paint_rect) - , canvas_rect(canvas_rect) - , device_scale(device_scale) - {} - ~CanvasItemBuffer() = default; - +struct CanvasItemBuffer { Geom::IntRect rect; - Geom::IntRect canvas_rect; // visible window in world coordinates (i.e. offset by _x0, _y0) int device_scale; // For high DPI monitors. - Cairo::RefPtr cr; - unsigned char *buf = nullptr; - int buf_rowstride = 0; - bool is_empty = true; }; } // Namespace Inkscape diff --git a/src/object/sp-namedview.cpp b/src/object/sp-namedview.cpp index f3c18f2153..d9cca37043 100644 --- a/src/object/sp-namedview.cpp +++ b/src/object/sp-namedview.cpp @@ -345,9 +345,6 @@ void SPNamedView::update(SPCtx *ctx, guint flags) } void SPNamedView::set(SPAttr key, const gchar* value) { - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - bool global_snapping = prefs->getBool("/options/snapdefault/value", true); - // Send page attributes to the page manager. if (this->_page_manager && this->_page_manager->subset(key, value)) { this->requestModified(SP_OBJECT_MODIFIED_FLAG); diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 6fa5c8b938..dd22ea161f 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -35,7 +35,7 @@ // Debugging switches #define ENABLE_FRAMECHECK 0 -#define ENABLE_LOGGING 1 +#define ENABLE_LOGGING 0 #define ENABLE_SLOW_REDRAW 1 #define ENABLE_SHOW_REDRAW 0 #define ENABLE_SHOW_UNCLEAN 1 @@ -121,7 +121,6 @@ namespace Widget { struct PaintRectSetup { gint64 start_time; - Geom::IntRect canvas_rect; int max_pixels; Geom::Point mouse_loc; bool disable_timeouts; @@ -140,6 +139,9 @@ private: Cairo::RefPtr _clean_region; ///< Subregion of store with up-to-date content. int _device_scale = 1; ///< Scale for high DPI montiors. Probably should be double. bool _store_solid_background; + Geom::Affine _store_affine; + + bool decoupled_mode = false; bool solid_background; @@ -156,6 +158,8 @@ private: void empty_bucket(); GdkEvent *ignore = nullptr; + + Geom::Affine geom_affine; }; void CanvasPrivate::schedule_bucket_emptier() @@ -266,7 +270,7 @@ Canvas::~Canvas() * Is world point inside canvas area? */ bool -Canvas::world_point_inside_canvas(Geom::Point const &world) +Canvas::world_point_inside_canvas(Geom::Point const &world) const { Gtk::Allocation allocation = get_allocation(); return ( _x0 <= world.x() && world.x() < _x0 + allocation.get_width() && @@ -277,7 +281,7 @@ Canvas::world_point_inside_canvas(Geom::Point const &world) * Translate point in canvas to world coordinates. */ Geom::Point -Canvas::canvas_to_world(Geom::Point const &point) +Canvas::canvas_to_world(Geom::Point const &point) const { return Geom::Point(point[Geom::X]+ _x0, point[Geom::Y] + _y0); } @@ -286,7 +290,7 @@ Canvas::canvas_to_world(Geom::Point const &point) * Return the area shown in the canvas in world coordinates. */ Geom::IntRect -Canvas::get_area_world() +Canvas::get_area_world() const { Gtk::Allocation allocation = get_allocation(); return Geom::IntRect::from_xywh(_x0, _y0, allocation.get_width(), allocation.get_height()); @@ -300,7 +304,6 @@ Canvas::set_affine(Geom::Affine const &affine) { if (_affine != affine) { _affine = affine; - _need_update = true; } } @@ -481,7 +484,7 @@ Canvas::set_split_direction(Inkscape::SplitDirection dir) } } -Cairo::RefPtr Canvas::get_backing_store() +Cairo::RefPtr Canvas::get_backing_store() const { return d->_backing_store; } @@ -588,9 +591,9 @@ Canvas::on_button_event(GdkEventButton *button_event) // Pick the current item as if the button were not pressed and then process event. _state = button_event->state; - pick_current_item(reinterpret_cast(button_event)); + pick_current_item(reinterpret_cast(button_event)); _state ^= mask; - retval = emit_event(reinterpret_cast(button_event)); + retval = emit_event(reinterpret_cast(button_event)); break; case GDK_BUTTON_RELEASE: @@ -599,10 +602,10 @@ Canvas::on_button_event(GdkEventButton *button_event) _split_dragging = false; _state = button_event->state; - retval = emit_event(reinterpret_cast(button_event)); + retval = emit_event(reinterpret_cast(button_event)); button_event->state ^= mask; _state = button_event->state; - pick_current_item(reinterpret_cast(button_event)); + pick_current_item(reinterpret_cast(button_event)); button_event->state ^= mask; break; @@ -659,13 +662,6 @@ Canvas::on_focus_in_event(GdkEventFocus *focus_event) return false; } -// TODO See if we still need this (canvas->focused_item removed between 0.48 and 0.91). -bool -Canvas::on_focus_out_event(GdkEventFocus *focus_event) -{ - return false; -} - // Actually, key events never reach here. bool Canvas::on_key_press_event(GdkEventKey *key_event) @@ -798,11 +794,28 @@ Canvas::on_motion_notify_event(GdkEventMotion *motion_event) return status; } -namespace { +auto +geom_to_cairo(Geom::IntRect rect) +{ + return Cairo::RectangleInt { rect.left(), rect.top(), rect.width(), rect.height() }; +} + +auto +cairo_to_geom(Cairo::RectangleInt rect) +{ + return Geom::IntRect::from_xywh(rect.x, rect.y, rect.width, rect.height); +} + auto geom_to_cairo(Geom::Affine affine) { return Cairo::Matrix(affine[0], affine[1], affine[2], affine[3], affine[4], affine[5]); } + +auto geom_act(Geom::Affine a, Geom::IntPoint p) +{ + Geom::Point p2 = p; + p2 *= a; + return Geom::IntPoint(std::round(p2.x()), std::round(p2.y())); } void @@ -853,21 +866,38 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->restore(); } - // Blit backing store to screen. - IF_FRAMECHECK( f = Prof("draw"); ) - cr->save(); - cr->set_operator(d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); - cr->set_source(d->_backing_store, d->_store_rect.left() - _x0, d->_store_rect.top() - _y0); - cr->paint(); - cr->restore(); + if (!d->decoupled_mode) { + // Blit backing store to screen. + IF_FRAMECHECK( f = Prof("draw"); ) + cr->save(); + cr->set_operator(d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); + cr->set_source(d->_backing_store, d->_store_rect.left() - _x0, d->_store_rect.top() - _y0); + cr->paint(); + cr->restore(); + } + else { + // Draw transformed store + cr->save(); + cr->translate(-_x0, -_y0); + cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); + cr->translate(d->_store_rect.left(), d->_store_rect.top()); + cr->set_source(d->_backing_store, 0, 0); + cr->set_operator(d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); + Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST); + cr->paint(); + cr->restore(); + } #if ENABLE_SHOW_UNCLEAN IF_FRAMECHECK( f = Prof("paint_unclean"); ) // Paint unclean regions in red. - auto reg = Cairo::Region::create( Cairo::RectangleInt{ _x0, _y0, get_allocation().get_width(), get_allocation().get_height() } ); + auto reg = Cairo::Region::create(geom_to_cairo(d->_store_rect)); reg->subtract(d->_clean_region); - reg->translate(-_x0, -_y0); cr->save(); + cr->translate(-_x0, -_y0); + if (d->decoupled_mode) { + cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); + } cr->set_source_rgba(1, 0, 0, 0.2); fill_cairo_region(cr, reg); cr->restore(); @@ -1026,18 +1056,6 @@ Canvas::add_idle() } } -auto -geom_to_cairo(Geom::IntRect rect) -{ - return Cairo::RectangleInt { rect.left(), rect.top(), rect.width(), rect.height() }; -} - -auto -cairo_to_geom(Cairo::RectangleInt rect) -{ - return Geom::IntRect::from_xywh(rect.x, rect.y, rect.width, rect.height); -} - auto distSq(const Geom::IntPoint pt, const Geom::IntRect &rect) { @@ -1059,92 +1077,133 @@ Canvas::on_idle() return false; } + // Get the device scale + const auto device_scale = get_scale_factor(); + + // Handle transitions in and out of decoupled mode + if (!d->decoupled_mode) { + // Enter decoupled mode if the affine has changed from what was last drawn (assuming there is anything) + if (d->_backing_store && _affine != d->_store_affine) { + d->decoupled_mode = true; + } + } + else { + // Exit decoupled mode if the store needs recreating + // todo: It would be nice if this also happened if none of the store was visible on the screen anymore. + // todo: Geom::Parallogram lets us check this + if (!d->_backing_store || d->_device_scale != device_scale || d->_store_solid_background != d->solid_background) { + d->decoupled_mode = false; + } + } + // Ensure geometry is up to date assert(_canvas_item_root); - if (_need_update) { - _canvas_item_root->update(_affine); + auto affine = d->decoupled_mode ? d->_store_affine : _affine; + if (_need_update || d->geom_affine != affine) { + d->geom_affine = affine; + _canvas_item_root->update(d->geom_affine); _need_update = false; } - // Get canvas rectangle in world coordinates - const auto canvas_rect = Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() ); - // Assert that _clean_region is a subregion of _store_rect auto tmp = d->_clean_region->copy(); tmp->subtract(geom_to_cairo(d->_store_rect)); assert(tmp->empty()); - // Ensure store contains canvas_rect - const auto pad = Geom::IntPoint(200, 200); - const auto device_scale = get_scale_factor(); + if (!d->decoupled_mode) { - if (!d->_backing_store || d->_device_scale != device_scale || d->_store_solid_background != d->solid_background || !d->_store_rect.intersects(canvas_rect)) - { - // Recreate the store, using the same memory if possible - d->_store_rect = Geom::IntRect::from_xywh( _x0, _y0, canvas_rect.width(), canvas_rect.height() ); - d->_store_rect.expandBy(pad); - d->_device_scale = device_scale; - d->_store_solid_background = d->solid_background; - int desired_width = d->_store_rect.width() * d->_device_scale; - int desired_height = d->_store_rect.height() * d->_device_scale; - if (!d->_backing_store || d->_backing_store->get_width() != desired_width || d->_backing_store->get_height() != desired_height) - d->_backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, desired_width, desired_height); - if (d->solid_background) { - auto cr = Cairo::Context::create(d->_backing_store); - cr->set_source(_background); - cr->set_operator(Cairo::OPERATOR_SOURCE); - cr->paint(); + // Get window rectangle in canvas coordinates + const auto canvas_rect = Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() ); + + // Ensure store contains canvas_rect + const auto pad = Geom::IntPoint(200, 200); + + if (!d->_backing_store || d->_device_scale != device_scale || d->_store_solid_background != d->solid_background || !d->_store_rect.intersects(canvas_rect)) + { + // Recreate the store, using the same memory if possible + d->_store_rect = Geom::IntRect::from_xywh( _x0, _y0, canvas_rect.width(), canvas_rect.height() ); + d->_store_rect.expandBy(pad); + d->_device_scale = device_scale; + d->_store_solid_background = d->solid_background; + d->_store_affine = _affine; + int desired_width = d->_store_rect.width() * d->_device_scale; + int desired_height = d->_store_rect.height() * d->_device_scale; + if (!d->_backing_store || d->_backing_store->get_width() != desired_width || d->_backing_store->get_height() != desired_height) + d->_backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, desired_width, desired_height); + if (d->solid_background) { + auto cr = Cairo::Context::create(d->_backing_store); + cr->set_source(_background); + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->paint(); + } + d->_clean_region = Cairo::Region::create(); + IF_SHOW_UNCLEAN( queue_draw() ); + + IF_LOGGING( std::cout << "Recreated store" << std::endl; ) } - d->_clean_region = Cairo::Region::create(); - IF_SHOW_UNCLEAN( queue_draw() ); + else if (!d->_store_rect.contains(canvas_rect)) + { + // Create new store, copy usable content across, set as new store + auto store_rect = Geom::IntRect::from_xywh( _x0, _y0, canvas_rect.width(), canvas_rect.height() ); + store_rect.expandBy(pad); + auto backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, store_rect.width() * d->_device_scale, store_rect.height() * d->_device_scale); + + auto shift = store_rect.min() - d->_store_rect.min(); + auto reuse_rect = store_rect & d->_store_rect; + assert(reuse_rect); + auto cr = Cairo::Context::create(backing_store); + + // Paint background where necessary + if (d->solid_background) { + auto reg = Cairo::Region::create(geom_to_cairo(store_rect)); + reg->subtract(geom_to_cairo(*reuse_rect)); + reg->translate(-store_rect.left(), -store_rect.top()); + cr->save(); + cr->set_source(_background); + cr->set_operator(Cairo::OPERATOR_SOURCE); + fill_cairo_region(cr, reg); + cr->restore(); + } - IF_LOGGING( std::cout << "Recreated store" << std::endl; ) - } - else if (!d->_store_rect.contains(canvas_rect)) - { - // Create new store, copy usable content across, set as new store - auto store_rect = Geom::IntRect::from_xywh( _x0, _y0, canvas_rect.width(), canvas_rect.height() ); - store_rect.expandBy(pad); - auto backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, store_rect.width() * d->_device_scale, store_rect.height() * d->_device_scale); - - auto shift = store_rect.min() - d->_store_rect.min(); - auto reuse_rect = store_rect & d->_store_rect; - assert(reuse_rect); - auto cr = Cairo::Context::create(backing_store); - - // Paint background where necessary - if (d->solid_background) { - auto reg = Cairo::Region::create(geom_to_cairo(store_rect)); - reg->subtract(geom_to_cairo(*reuse_rect)); - reg->translate(-store_rect.left(), -store_rect.top()); + // Copy usuable contents of store shifted cr->save(); - cr->set_source(_background); + cr->rectangle(reuse_rect->left() - store_rect.left(), reuse_rect->top() - store_rect.top(), reuse_rect->width(), reuse_rect->height()); + cr->clip(); + cr->set_source(d->_backing_store, -shift.x(), -shift.y()); cr->set_operator(Cairo::OPERATOR_SOURCE); - fill_cairo_region(cr, reg); + cr->paint(); cr->restore(); + + d->_store_rect = store_rect; + d->_backing_store = std::move(backing_store); + d->_clean_region->intersect(geom_to_cairo(d->_store_rect)); + IF_SHOW_UNCLEAN( queue_draw() ); + + IF_LOGGING( std::cout << "Shifted store" << std::endl; ) } - // Copy usuable contents of store shifted - cr->save(); - cr->rectangle(reuse_rect->left() - store_rect.left(), reuse_rect->top() - store_rect.top(), reuse_rect->width(), reuse_rect->height()); - cr->clip(); - cr->set_source(d->_backing_store, -shift.x(), -shift.y()); - cr->set_operator(Cairo::OPERATOR_SOURCE); - cr->paint(); - cr->restore(); + assert(d->_store_rect.contains(canvas_rect)); + } - d->_store_rect = store_rect; - d->_backing_store = std::move(backing_store); - d->_clean_region->intersect(geom_to_cairo(d->_store_rect)); - IF_SHOW_UNCLEAN( queue_draw() ); + // Get region that requires painting + Cairo::RefPtr region; + if (!d->decoupled_mode) { + // Get the window rectangle transformed into canvas space again + const auto canvas_rect = Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() ); - IF_LOGGING( std::cout << "Shifted store" << std::endl; ) + region = Cairo::Region::create(geom_to_cairo(canvas_rect)); } + else { + // We get to choose how large this region should be. It could even be infinite, and things would still work. But it is desirable to make it as small as possible + // while still containing the window rectangle transformed into canvas space. - assert(d->_store_rect.contains(canvas_rect)); + // Currently, as a placeholder, I just set it to be something fixed and very large: + region = Cairo::Region::create(Cairo::RectangleInt { -100000, -100000, 200000, 200000 } ); - // Get region that requires painting - auto region = Cairo::Region::create(geom_to_cairo(canvas_rect)); + // In the future we could use the bounding box of the transformed window, or a coarsened version of the actual rectangle. + // But both of these are far too fiddly for me to attempt in this initial pass. + } + region->intersect(geom_to_cairo(d->_store_rect)); // for sanity region->subtract(d->_clean_region); // Get mouse position in canvas space @@ -1154,10 +1213,15 @@ Canvas::on_idle() int y; Gdk::ModifierType mask; window->get_device_position(Gdk::Display::get_default()->get_default_seat()->get_pointer(), x, y, mask); - mouse_loc = Geom::IntPoint(_x0 + x, _y0 + y); + mouse_loc = {x, y}; } else { - mouse_loc = canvas_rect.midpoint(); + mouse_loc = {get_allocation().get_width() / 2, get_allocation().get_height() / 2}; + } + + mouse_loc += Geom::IntPoint(_x0, _y0); + if (d->decoupled_mode) { + mouse_loc = geom_act(d->_store_affine * _affine.inverse(), mouse_loc); } // Obtain rectangles list sorted by distance from mouse @@ -1170,7 +1234,6 @@ Canvas::on_idle() // Set up painting info to pass down PaintRectSetup setup; - setup.canvas_rect = canvas_rect; setup.mouse_loc = mouse_loc; auto prefs = Inkscape::Preferences::get(); @@ -1189,11 +1252,11 @@ Canvas::on_idle() setup.disable_timeouts = false; // TEMP HACK: I WANT TO INVESTIGATE BEHAVIOUR WITHOUT THIS INTERFERING for (const auto &rect : rects) { - auto area = rect & canvas_rect; + //auto area = rect & canvas_rect; // This is strictly not necessary. I am disabling it for now, and will probably remove it completely in a little while. - if (!area || area->hasZeroArea()) { + /*if (!area || area->hasZeroArea()) { // The same probably goes for this; though it would be worth a check. continue; - } + }*/ if (!paint_rect_internal(setup, rect)) { // Timed out. Temporarily return to idle loop, and come back here if still idle. @@ -1219,7 +1282,16 @@ Canvas::on_idle() } } - // todo: check clean region is what it should be (should have finished drawing by now) + // Finished drawing - see if we need to exit decoupled mode and do a final redraw + if (d->decoupled_mode) { + // Todo: save the current store as a snapshot store to be rendered to back + // do this by swapping the store and the snapshot + + // BTW this is not the right way to do this, and leads to frequent crashes. but it demos the concept. + /*d->_backing_store.clear(); + d->decoupled_mode = false; + return true;*/ + } IF_FRAMECHECK( f.subtype = 3; ) return false; @@ -1279,7 +1351,7 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th _drawing->setRenderMode(_render_mode); _drawing->setColorMode(_color_mode); - paint_single_buffer(this_rect, setup.canvas_rect, d->_backing_store); + paint_single_buffer(this_rect, d->_backing_store); /*bool outline_overlay = _drawing->outlineOverlay(); if (_split_mode != Inkscape::SplitMode::NORMAL || outline_overlay) { @@ -1297,7 +1369,15 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th Cairo::RectangleInt crect = { this_rect.left(), this_rect.top(), this_rect.width(), this_rect.height() }; d->_clean_region->do_union( crect ); - queue_draw_area(this_rect.left() - _x0, this_rect.top() - _y0, this_rect.width(), this_rect.height()); + if (!d->decoupled_mode) { + queue_draw_area(this_rect.left() - _x0, this_rect.top() - _y0, this_rect.width(), this_rect.height()); + } + else { + // In decoupled mode, we have to calculate the bounding box of this_rect transformed from canvas space to screen space, rounded outwards, and invalidate that. + // Because this is tricky, and not really necessary in a first pass, here I invalidate the whole window. It should not lead to any performance impact on modern compositors modulo gtk/gdk inefficiencies. + queue_draw(); + } + d->pending_draw = true; if (!setup.disable_timeouts) { @@ -1366,8 +1446,7 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th * store: Cairo surface to draw on. */ void -Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Geom::IntRect const &canvas_rect, - Cairo::RefPtr &store) +Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtr &store) { // Make sure the following code does not go outside of store's data assert(store); @@ -1413,8 +1492,7 @@ Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Geom::IntRect const // Render drawing on top of background. if (_canvas_item_root->is_visible()) { - Inkscape::CanvasItemBuffer buf(paint_rect, d->_store_rect, d->_device_scale); - buf.cr = cr; + auto buf = Inkscape::CanvasItemBuffer{ paint_rect, d->_device_scale, cr }; _canvas_item_root->render(&buf); } @@ -1545,8 +1623,10 @@ bool Canvas::pick_current_item(GdkEvent *event) { // Ensure geometry is correct. - if (_need_update) { - _canvas_item_root->update(_affine); + auto affine = d->decoupled_mode ? d->_store_affine : _affine; + if (_need_update || d->geom_affine != affine) { + d->geom_affine = affine; + _canvas_item_root->update(d->geom_affine); _need_update = false; } diff --git a/src/ui/widget/canvas.h b/src/ui/widget/canvas.h index ca651eff8d..8114842da1 100644 --- a/src/ui/widget/canvas.h +++ b/src/ui/widget/canvas.h @@ -54,14 +54,15 @@ public: // Structure TODO: Remove desktop dependency. void set_desktop(SPDesktop *desktop) { _desktop = desktop; } - SPDesktop *get_desktop() { return _desktop; } + SPDesktop *get_desktop() const { return _desktop; } // Geometry - bool world_point_inside_canvas(Geom::Point const &world); // desktop-events.cpp - Geom::Point canvas_to_world(Geom::Point const &window); - Geom::IntRect get_area_world(); + bool world_point_inside_canvas(Geom::Point const &world) const; // desktop-events.cpp + Geom::Point canvas_to_world(Geom::Point const &window) const; + Geom::IntRect get_area_world() const; + void set_affine(Geom::Affine const &affine); - Geom::Affine get_affine() { return _affine; } + Geom::Affine get_affine() const { return _affine; } // Drawing void set_drawing(Inkscape::Drawing *drawing) { _drawing = drawing; } @@ -74,40 +75,40 @@ public: void set_background_checkerboard(guint32 rgba = 0xC4C4C4FF); void set_drawing_disabled(bool disable) { _drawing_disabled = disable; } // Disable during path ops, etc. - bool is_dragging() { return _is_dragging; } // selection-chemistry.cpp + bool is_dragging() const { return _is_dragging; } // selection-chemistry.cpp // Rendering modes void set_render_mode(Inkscape::RenderMode mode); void set_color_mode( Inkscape::ColorMode mode); void set_split_mode( Inkscape::SplitMode mode); void set_split_direction(Inkscape::SplitDirection dir); - Inkscape::RenderMode get_render_mode() { return _render_mode; } - Inkscape::ColorMode get_color_mode() { return _color_mode; } - Inkscape::SplitMode get_split_mode() { return _split_mode; } + Inkscape::RenderMode get_render_mode() const { return _render_mode; } + Inkscape::ColorMode get_color_mode() const { return _color_mode; } + Inkscape::SplitMode get_split_mode() const { return _split_mode; } void set_cms_key(std::string key) { _cms_key = key; _cms_active = !key.empty(); } - std::string get_cms_key() { return _cms_key; } + std::string get_cms_key() const { return _cms_key; } void set_cms_active(bool active) { _cms_active = active; } - bool get_cms_active() { return _cms_active; } + bool get_cms_active() const { return _cms_active; } - Cairo::RefPtr get_backing_store(); // Background rotation preview - Cairo::RefPtr get_background_pattern() { return _background; } + Cairo::RefPtr get_backing_store() const; // Background rotation preview + Cairo::RefPtr get_background_pattern() const { return _background; } // For a GTK bug (see SelectedStyle::on_opacity_changed()). void forced_redraws_start(int count, bool reset = true); void forced_redraws_stop() { _forced_redraw_limit = -1; } // Canvas Items - CanvasItemGroup *get_canvas_item_root() { return _canvas_item_root; } + CanvasItemGroup *get_canvas_item_root() const { return _canvas_item_root; } - Inkscape::CanvasItem *get_current_canvas_item() { return _current_canvas_item; } + Inkscape::CanvasItem *get_current_canvas_item() const { return _current_canvas_item; } void set_current_canvas_item(Inkscape::CanvasItem *item) { _current_canvas_item = item; } - Inkscape::CanvasItem *get_grabbed_canvas_item() { return _grabbed_canvas_item; } + Inkscape::CanvasItem *get_grabbed_canvas_item() const { return _grabbed_canvas_item; } void set_grabbed_canvas_item(Inkscape::CanvasItem *item, Gdk::EventMask mask) { _grabbed_canvas_item = item; _grabbed_event_mask = mask; @@ -132,7 +133,6 @@ protected: bool on_enter_notify_event( GdkEventCrossing *crossing_event) override; bool on_leave_notify_event( GdkEventCrossing *crossing_event) override; bool on_focus_in_event( GdkEventFocus *focus_event ) override; - bool on_focus_out_event( GdkEventFocus *focus_event ) override; bool on_key_press_event( GdkEventKey *key_event ) override; bool on_key_release_event( GdkEventKey *key_event ) override; bool on_motion_notify_event( GdkEventMotion *motion_event) override; @@ -157,8 +157,7 @@ private: // In order they are called in painting. bool paint(); bool paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &this_rect); - void paint_single_buffer(Geom::IntRect const &paint_rect, Geom::IntRect const &canvas_rect, - Cairo::RefPtr &store); + void paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtr &store); void add_clippath(const Cairo::RefPtr& cr); void set_cursor(); @@ -177,7 +176,6 @@ private: // Geometry int _x0 = 0, _y0 = 0; ///< Coordinates of top-left pixel of canvas view within canvas. Geom::Affine _affine; // Only used for canvas items at the moment. - bool _in_full_redraw = false; // Hack used to lower idle priority for full redraws. // Event handling/item picking GdkEvent _pick_event; ///< Event used to find currently selected item. diff --git a/src/widgets/desktop-widget.cpp b/src/widgets/desktop-widget.cpp index 8328738c8e..ef0bf0e45f 100644 --- a/src/widgets/desktop-widget.cpp +++ b/src/widgets/desktop-widget.cpp @@ -275,7 +275,6 @@ SPDesktopWidget::SPDesktopWidget() } } else if (auto sep = dynamic_cast(widget)) { - auto parent = sep->get_parent(); if (buttons_before_separator <= 0) { sep->hide(); } -- GitLab From 0edd044922b62b3dc97ebf6179bb6e3ab528ab80 Mon Sep 17 00:00:00 2001 From: PBS Date: Thu, 13 Jan 2022 13:02:51 +0900 Subject: [PATCH 08/35] Implement placeholders --- src/ui/widget/canvas.cpp | 92 +++++++++++++++++++++++++++------- src/widgets/desktop-widget.cpp | 4 +- 2 files changed, 77 insertions(+), 19 deletions(-) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index dd22ea161f..42770906fe 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -160,6 +160,8 @@ private: GdkEvent *ignore = nullptr; Geom::Affine geom_affine; + + void queue_draw_area(Geom::IntRect &rect); }; void CanvasPrivate::schedule_bucket_emptier() @@ -218,6 +220,11 @@ void CanvasPrivate::empty_bucket() } } +void CanvasPrivate::queue_draw_area(Geom::IntRect &rect) +{ + q->queue_draw_area(rect.left(), rect.top(), rect.width(), rect.height()); +} + Canvas::Canvas() : _size_observer(this, "/options/grabsize/value"), d(std::make_unique()) { @@ -1063,6 +1070,14 @@ distSq(const Geom::IntPoint pt, const Geom::IntRect &rect) return v.x() * v.x() + v.y() * v.y(); } +auto +empty_int_rect_hack() +{ + auto min = std::numeric_limits::min(); + auto interval = Geom::GenericInterval(min, min); + return Geom::IntRect(interval, interval); +} + bool Canvas::on_idle() { @@ -1087,12 +1102,28 @@ Canvas::on_idle() d->decoupled_mode = true; } } - else { - // Exit decoupled mode if the store needs recreating - // todo: It would be nice if this also happened if none of the store was visible on the screen anymore. - // todo: Geom::Parallogram lets us check this + else { // if (d->decoupled_mode) + bool exit = false; + if (!d->_backing_store || d->_device_scale != device_scale || d->_store_solid_background != d->solid_background) { + // Exit decoupled mode if the store needs recreating + exit = true; + } + else { + // Also exit if the store has completely gone off the screen + auto pl = Geom::Parallelogram(get_area_world()); + pl *= d->_store_affine * _affine.inverse(); + if (!pl.intersects(d->_store_rect)) { + exit = true; + } + } + + if (exit) { d->decoupled_mode = false; + + // Mark store as containing no valid pixels, hence needing recreation + d->_store_rect = empty_int_rect_hack(); + d->_clean_region = Cairo::Region::create(); } } @@ -1192,18 +1223,27 @@ Canvas::on_idle() const auto canvas_rect = Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() ); region = Cairo::Region::create(geom_to_cairo(canvas_rect)); + assert(d->_store_rect.contains(canvas_rect)); // Painting region already guaranteed to lie within store } else { - // We get to choose how large this region should be. It could even be infinite, and things would still work. But it is desirable to make it as small as possible - // while still containing the window rectangle transformed into canvas space. + // Get the window rectangle transformed into canvas space + auto pl = Geom::Parallelogram(Geom::IntRect(0, 0, get_allocation().get_width(), get_allocation().get_height())); + pl *= Geom::Translate(_x0, _y0); + pl *= d->_store_affine * _affine.inverse(); + + // Get bounding box, round outwards + auto b = pl.bounds(); + auto bi = Geom::IntRect(b.min().floor(), b.max().ceil()); - // Currently, as a placeholder, I just set it to be something fixed and very large: - region = Cairo::Region::create(Cairo::RectangleInt { -100000, -100000, 200000, 200000 } ); + // Set as painting region + region = Cairo::Region::create(geom_to_cairo(bi)); - // In the future we could use the bounding box of the transformed window, or a coarsened version of the actual rectangle. - // But both of these are far too fiddly for me to attempt in this initial pass. + // Todo: In the future, consider coarsening pl into a region rather than taking its bounding box. + // This reduces the area to paint, but increases the number of rectangles, so is NOT necessarily an optimisation, and could be a pessimisation instead. + + // Painting region MUST lie within store, so clip it if necessary + region->intersect(geom_to_cairo(d->_store_rect)); } - region->intersect(geom_to_cairo(d->_store_rect)); // for sanity region->subtract(d->_clean_region); // Get mouse position in canvas space @@ -1370,16 +1410,34 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th d->_clean_region->do_union( crect ); if (!d->decoupled_mode) { - queue_draw_area(this_rect.left() - _x0, this_rect.top() - _y0, this_rect.width(), this_rect.height()); + // Get rectangle needing repaint + auto repaint_rect = this_rect - Geom::IntPoint(_x0, _y0); + + // Assert that a repaint actually occurs (guaranteed because we are only asked to paint fully on-screen rectangles) + auto screen_rect = Geom::IntRect(0, 0, get_allocation().get_width(), get_allocation().get_height()); + assert(repaint_rect & screen_rect); + + // Schedule repaint + d->queue_draw_area(repaint_rect); + d->pending_draw = true; } else { - // In decoupled mode, we have to calculate the bounding box of this_rect transformed from canvas space to screen space, rounded outwards, and invalidate that. - // Because this is tricky, and not really necessary in a first pass, here I invalidate the whole window. It should not lead to any performance impact on modern compositors modulo gtk/gdk inefficiencies. - queue_draw(); + // Get rectangle needing repaint (transform into screen space, take bounding box, round outwards) + auto pl = Geom::Parallelogram(this_rect); + pl *= _affine * d->_store_affine.inverse(); + pl *= Geom::Translate(-_x0, -_y0); + auto b = pl.bounds(); + auto repaint_rect = Geom::IntRect(b.min().floor(), b.max().ceil()); + + // Check if repaint is necessary - some rectangles could be entirely off-screen + auto screen_rect = Geom::IntRect(0, 0, get_allocation().get_width(), get_allocation().get_height()); + if (repaint_rect & screen_rect) { + // Schedule repaint + d->queue_draw_area(repaint_rect); + d->pending_draw = true; + } } - d->pending_draw = true; - if (!setup.disable_timeouts) { auto now = g_get_monotonic_time(); auto elapsed = now - setup.start_time; diff --git a/src/widgets/desktop-widget.cpp b/src/widgets/desktop-widget.cpp index ef0bf0e45f..d6a5c0e6d2 100644 --- a/src/widgets/desktop-widget.cpp +++ b/src/widgets/desktop-widget.cpp @@ -1747,8 +1747,8 @@ SPDesktopWidget::update_scrollbars(double scale) /* Canvas region we always show unconditionally */ double const y_dir = desktop->yaxisdir(); - Geom::Rect carea( Geom::Point(deskarea->left() * scale - 64, (deskarea->top() * scale + 64) * y_dir), - Geom::Point(deskarea->right() * scale + 64, (deskarea->bottom() * scale - 64) * y_dir) ); + Geom::Rect carea( Geom::Point(deskarea->left() * scale - 64, (deskarea->top() * scale + 64) * y_dir), + Geom::Point(deskarea->right() * scale + 64, (deskarea->bottom() * scale - 64) * y_dir) ); Geom::Rect viewbox = _canvas->get_area_world(); -- GitLab From 514d319bde9e0b711051bd89f1e87960beadafdd Mon Sep 17 00:00:00 2001 From: PBS Date: Fri, 14 Jan 2022 00:47:32 +0900 Subject: [PATCH 09/35] Finish decoupled mode. Seems to be behaving itself. --- src/ui/widget/canvas.cpp | 657 ++++++++++++++++++++------------------- 1 file changed, 340 insertions(+), 317 deletions(-) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 42770906fe..5b59c34725 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -34,11 +34,12 @@ #include "ui/tools/tool-base.h" // Default cursor // Debugging switches -#define ENABLE_FRAMECHECK 0 -#define ENABLE_LOGGING 0 -#define ENABLE_SLOW_REDRAW 1 +#define ENABLE_FRAMECHECK 1 +#define ENABLE_LOGGING 1 +#define ENABLE_OVERBISECTION 1 +#define ENABLE_SLOW_REDRAW 0 #define ENABLE_SHOW_REDRAW 0 -#define ENABLE_SHOW_UNCLEAN 1 +#define ENABLE_SHOW_UNCLEAN 0 #if ENABLE_FRAMECHECK #include "../../../../framecheck.h" @@ -53,8 +54,11 @@ #define IF_LOGGING(X) #endif +#if ENABLE_OVERBISECTION +#define OVERBISECTION_SIZE 400 // 30 // px +#endif + #if ENABLE_SLOW_REDRAW -#define SLOW_REDRAW_SIZE 30 // px #define SLOW_REDRAW_TIME 50 // us #endif @@ -119,10 +123,13 @@ namespace Inkscape { namespace UI { namespace Widget { +static constexpr int RENDER_TIME_LIMIT = 1000; // Render time limit, in microseconds. +static constexpr double MAX_AFFINE_DIFF = 1.0; // Threshold for redraw cancel and restart. + struct PaintRectSetup { - gint64 start_time; + Geom::IntPoint mouse_loc; int max_pixels; - Geom::Point mouse_loc; + gint64 start_time; bool disable_timeouts; }; @@ -133,13 +140,19 @@ private: friend class Canvas; Canvas *q; - Cairo::RefPtr _backing_store; ///< Canvas content. - Cairo::RefPtr _outline_store; ///< Canvas outline content; only exists in split/x-ray mode. - Geom::IntRect _store_rect; ///< Rectangle of the store in world space. - Cairo::RefPtr _clean_region; ///< Subregion of store with up-to-date content. - int _device_scale = 1; ///< Scale for high DPI montiors. Probably should be double. + // Important global properties of all the stores. If these change, all the stores must be recreated. + int _device_scale = 1; bool _store_solid_background; + + Geom::IntRect _store_rect; ///< Rectangle of the store in world space. Geom::Affine _store_affine; + Cairo::RefPtr _backing_store; ///< Canvas content. + Cairo::RefPtr _clean_region; ///< Subregion of backing store with up-to-date content. + + Geom::IntRect _snapshot_rect; + Geom::Affine _snapshot_affine; + Cairo::RefPtr _snapshot_store; + Cairo::RefPtr _snapshot_clean_region; bool decoupled_mode = false; @@ -212,7 +225,7 @@ void CanvasPrivate::empty_bucket() if (!finished) { - // UNACCEPTABLE HACK!!! BUT I HAVE NO CHOICE + // Re-fire the event at the window, and ignore it when it comes back here again ignore = event.get(); q->get_toplevel()->event(event.get()); ignore = nullptr; @@ -257,7 +270,9 @@ Canvas::Canvas() _canvas_item_root->set_name("CanvasItemGroup:Root"); _canvas_item_root->set_canvas(this); + #if ENABLE_SHOW_REDRAW srand(g_get_monotonic_time()); + #endif d->q = this; } @@ -304,14 +319,19 @@ Canvas::get_area_world() const } /** - * Set the affine for the canvas and flag need for geometry update. + * Set the affine for the canvas. */ void Canvas::set_affine(Geom::Affine const &affine) { - if (_affine != affine) { - _affine = affine; + if (_affine == affine) { + return; } + + _affine = affine; + + add_idle(); + queue_draw(); } /** @@ -326,8 +346,8 @@ Canvas::redraw_all() return; } d->_clean_region = Cairo::Region::create(); // Empty region (i.e. everything is dirty). - IF_SHOW_UNCLEAN( queue_draw() ); add_idle(); + IF_SHOW_UNCLEAN( queue_draw() ); } /** @@ -359,8 +379,8 @@ Canvas::redraw_area(int x0, int y0, int x1, int y1) Cairo::RectangleInt crect = { x0, y0, x1-x0, y1-y0 }; d->_clean_region->subtract(crect); - IF_SHOW_UNCLEAN( queue_draw() ); add_idle(); + IF_SHOW_UNCLEAN( queue_draw() ); } void @@ -392,8 +412,11 @@ Canvas::redraw_area(Geom::Rect& area) void Canvas::request_update() { + // Flag geometry as needing update. _need_update = true; - add_idle(); // Geometry changed, need to redraw. + + // Trigger the idle process to perform the update. + add_idle(); } /** @@ -826,12 +849,11 @@ auto geom_act(Geom::Affine a, Geom::IntPoint p) } void -fill_cairo_region(const Cairo::RefPtr &cr, const Cairo::RefPtr ®) +region_to_path(const Cairo::RefPtr &cr, const Cairo::RefPtr ®) { for (int i = 0; i < reg->get_num_rectangles(); i++) { auto rect = reg->get_rectangle(i); cr->rectangle(rect.x, rect.y, rect.width, rect.height); - cr->fill(); } } @@ -859,10 +881,10 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) // sp_canvas_item_recursive_print_tree(0, _root); // canvas_item_print_tree(_canvas_item_root); - assert(d->_backing_store/* && _outline_store*/); + assert(d->_backing_store); assert(_drawing); - // Blit background (e.g. checkerboard). + // Blit background if not solid. (If solid, it is baked into the stores.) if (!d->solid_background) { IF_FRAMECHECK( f = Prof("background"); ) @@ -883,12 +905,57 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->restore(); } else { - // Draw transformed store + // Todo: Cairo does not seem to allow antialiasing for clipping to be disabled. This is completely unwanted, + // but is killing performance, as well as resulting in line artifacts and allowing garbage data to bleed through, + // which we would otherwise be able to guarantee didn't happen. + // Related: https://stackoverflow.com/questions/57390954/why-is-clipping-considered-to-be-too-slow-with-cairo + // Solution: consider bypassing Cairo just for this final compositing step. + + // Blit background to complement of both clean regions, if solid (and therefore not already drawn). + if (d->solid_background) { + IF_FRAMECHECK( f = Prof("composite", 2); ) + cr->save(); + cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD); + cr->rectangle(0, 0, get_allocation().get_width(), get_allocation().get_height()); + cr->translate(-_x0, -_y0); + cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); + region_to_path(cr, d->_clean_region); + cr->transform(geom_to_cairo(d->_store_affine * d->_snapshot_affine.inverse())); + region_to_path(cr, d->_snapshot_clean_region); + cr->clip(); + cr->set_source(_background); + cr->set_operator(Cairo::OPERATOR_SOURCE); + Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST); + cr->paint(); + cr->restore(); + } + + // Draw transformed snapshot, clipped to its clean region and the complement of the backing store's clean region. + IF_FRAMECHECK( f = Prof("composite", 1); ) cr->save(); + cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD); + cr->rectangle(0, 0, get_allocation().get_width(), get_allocation().get_height()); cr->translate(-_x0, -_y0); cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); - cr->translate(d->_store_rect.left(), d->_store_rect.top()); - cr->set_source(d->_backing_store, 0, 0); + region_to_path(cr, d->_clean_region); + cr->clip(); + cr->transform(geom_to_cairo(d->_store_affine * d->_snapshot_affine.inverse())); + region_to_path(cr, d->_snapshot_clean_region); + cr->clip(); + cr->set_source(d->_snapshot_store, d->_snapshot_rect.left(), d->_snapshot_rect.top()); + cr->set_operator(d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); + Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST); + cr->paint(); + cr->restore(); + + // Draw transformed store, clipped to clean region. + IF_FRAMECHECK( f = Prof("composite", 0); ) + cr->save(); + cr->translate(-_x0, -_y0); + cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); + region_to_path(cr, d->_clean_region); + cr->clip(); + cr->set_source(d->_backing_store, d->_store_rect.left(), d->_store_rect.top()); cr->set_operator(d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST); cr->paint(); @@ -906,7 +973,8 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); } cr->set_source_rgba(1, 0, 0, 0.2); - fill_cairo_region(cr, reg); + region_to_path(cr, reg); + cr->fill(); cr->restore(); #endif @@ -916,124 +984,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) d->pending_draw = false; } -/* - // Draw overlay if required. - if (_drawing->outlineOverlay()) { - - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - double outline_overlay_opacity = 1 - (prefs->getIntLimited("/options/rendering/outline-overlay-opacity", 50, 1, 100) / 100.0); - - // Partially obscure drawing by painting semi-transparent white. - cr->set_source_rgb(255,255,255); - cr->paint_with_alpha(outline_overlay_opacity); - - // Overlay outline - cr->set_source(_outline_store, 0, 0); - cr->paint(); - } - - // Draw split if required. - if (_split_mode != Inkscape::SplitMode::NORMAL) { - - // Move split position to center if not in canvas. - auto const rect = Geom::Rect(0, 0, _width, _height); - if (!rect.contains(_split_position)) { - _split_position = rect.midpoint(); - } - - // Add clipping path and blit background. - cr->save(); - cr->set_operator(Cairo::OPERATOR_SOURCE); - cr->set_source(_background); - add_clippath(cr); - cr->paint(); - cr->restore(); - - // Add clipping path and blit outline store. - cr->save(); - cr->set_source(_outline_store, 0, 0); - add_clippath(cr); - cr->paint(); - cr->restore(); - } - - if (_split_mode == Inkscape::SplitMode::SPLIT) { - - // Add dividing line. - cr->save(); - cr->set_source_rgb(0, 0, 0); - cr->set_line_width(1); - if (_split_direction == Inkscape::SplitDirection::EAST || - _split_direction == Inkscape::SplitDirection::WEST) { - cr->move_to((int)_split_position.x() + 0.5, 0); - cr->line_to((int)_split_position.x() + 0.5, _allocation.get_height()); - cr->stroke(); - } else { - cr->move_to( 0, (int)_split_position.y() + 0.5); - cr->line_to(_allocation.get_width(), (int)_split_position.y() + 0.5); - cr->stroke(); - } - cr->restore(); - - // Add controller image. - double a = _hover_direction == Inkscape::SplitDirection::NONE ? 0.5 : 1.0; - cr->save(); - cr->set_source_rgba(0.2, 0.2, 0.2, a); - cr->arc(_split_position.x(), _split_position.y(), 20 * _device_scale, 0, 2 * M_PI); - cr->fill(); - cr->restore(); - - cr->save(); - for (int i = 0; i < 4; ++i) { - // The four direction triangles. - cr->save(); - - // Position triangle. - cr->translate(_split_position.x(), _split_position.y()); - cr->rotate((i+2)*M_PI/2.0); - - // Draw triangle. - cr->move_to(-5 * _device_scale, 8 * _device_scale); - cr->line_to( 0, 18 * _device_scale); - cr->line_to( 5 * _device_scale, 8 * _device_scale); - cr->close_path(); - - double b = (int)_hover_direction == (i+1) ? 0.9 : 0.7; - cr->set_source_rgba(b, b, b, a); - cr->fill(); - - cr->restore(); - } - cr->restore(); - }*/ - - // static int i = 0; - // ++i; - // std::string file = "on_draw_" + std::to_string(i) + ".png"; - // _backing_store->write_to_png(file); -/* - // This whole section is just to determine if we call add_idle! - auto dirty_region = Cairo::Region::create(); - - std::vector clip_rectangles; - cr->copy_clip_rectangle_list(clip_rectangles); - for (auto & rectangle : clip_rectangles) { - Geom::Rect dr = Geom::Rect::from_xywh(rectangle.x + _x0, - rectangle.y + _y0, - rectangle.width, - rectangle.height); - // "rectangle" is floating point, we must convert to integer. We round outward as it's - // better to have a larger dirty region to avoid artifacts. - Geom::IntRect ir = dr.roundOutwards(); - Cairo::RectangleInt irect = { ir.left(), ir.top(), ir.width(), ir.height() }; - dirty_region->do_union(irect); - } - - dirty_region->subtract(_clean_region); - - if (!dirty_region->empty()) { - add_idle(); - }*/ + // Todo: Add back X-ray view. (Should be easy.) return true; } @@ -1078,261 +1029,333 @@ empty_int_rect_hack() return Geom::IntRect(interval, interval); } +auto +calc_affine_diff(const Geom::Affine &a, const Geom::Affine &b) { + auto c = a.inverse() * b; + return std::abs(c[0] - 1) + std::abs(c[1]) + std::abs(c[2]) + std::abs(c[3] - 1); +} + +auto +region_to_rects(const Cairo::RefPtr ®ion) +{ + std::vector rects; + int nrects = region->get_num_rectangles(); + rects.reserve(nrects); + for (int i = 0; i < nrects; i++) { + rects.emplace_back(cairo_to_geom(region->get_rectangle(i))); + } + return rects; +} + bool Canvas::on_idle() { IF_FRAMECHECK( auto f = FuncProf(); ) + assert(_canvas_item_root); + if (_in_destruction) { std::cerr << "Canvas::on_idle: Called after canvas destroyed!" << std::endl; + return false; } - // Quit idle process if not supposed to be drawing + // Quit idle process if not supposed to be drawing. if (!_drawing || _drawing_disabled) { return false; } - // Get the device scale - const auto device_scale = get_scale_factor(); + const Geom::IntPoint pad(200, 200); // Todo: Tune. + auto recreate_store = [&, this] { + // Recreate the store at the current affine so that it covers the visible region. + d->_store_rect = Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() ); + d->_store_rect.expandBy(pad); + d->_store_affine = _affine; + int desired_width = d->_store_rect.width() * d->_device_scale; + int desired_height = d->_store_rect.height() * d->_device_scale; + if (!d->_backing_store || d->_backing_store->get_width() != desired_width || d->_backing_store->get_height() != desired_height) { + // Todo: Stop cairo unnecessarily pre-filling the store with transparency below if d->solid_background is true. + d->_backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, desired_width, desired_height); + } + auto cr = Cairo::Context::create(d->_backing_store); + if (d->solid_background) { + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->set_source(_background); + } + else { + cr->set_operator(Cairo::OPERATOR_CLEAR); + } + cr->paint(); + d->_clean_region = Cairo::Region::create(); + IF_SHOW_UNCLEAN( queue_draw() ); + }; + + // Determine the rendering parameters have changed, and reset if so. + if (!d->_backing_store || d->_device_scale != get_scale_factor() || d->_store_solid_background != d->solid_background) { + d->_device_scale = get_scale_factor(); + d->_store_solid_background = d->solid_background; + recreate_store(); + d->decoupled_mode = false; + IF_LOGGING( std::cout << "Full reset" << std::endl; ) + } + + // Todo: Consider incrementally and pre-emptively performing this operation across several frames to avoid lag spikes. + auto shift_store = [&, this] { + // Recreate the store, but keep re-usable content from the old store. + auto store_rect = Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() ); + store_rect.expandBy(pad); + // Todo: Stop cairo unnecessarily pre-filling the store with transparency below if d->solid_background is true. + auto backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, store_rect.width() * d->_device_scale, store_rect.height() * d->_device_scale); + + // Determine the geometry of the shift. + auto shift = store_rect.min() - d->_store_rect.min(); + auto reuse_rect = store_rect & d->_store_rect; + assert(reuse_rect); // Should not be called if there is no overlap. + auto cr = Cairo::Context::create(backing_store); + + // Paint background into region not covered by next operation. + if (d->solid_background) { + auto reg = Cairo::Region::create(geom_to_cairo(store_rect)); + reg->subtract(geom_to_cairo(*reuse_rect)); + reg->translate(-store_rect.left(), -store_rect.top()); + cr->save(); + if (d->solid_background) { + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->set_source(_background); + } + else { + cr->set_operator(Cairo::OPERATOR_CLEAR); + } + region_to_path(cr, reg); + cr->fill(); + cr->restore(); + } + + // Copy re-usuable contents of old store into new store, shifted. + cr->save(); + cr->rectangle(reuse_rect->left() - store_rect.left(), reuse_rect->top() - store_rect.top(), reuse_rect->width(), reuse_rect->height()); + cr->clip(); + cr->set_source(d->_backing_store, -shift.x(), -shift.y()); + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->paint(); + cr->restore(); + + // Set the result as the new backing store. + d->_store_rect = store_rect; + assert(d->_store_affine == _affine); // Should not be called if the affine has changed. + d->_backing_store = std::move(backing_store); + d->_clean_region->intersect(geom_to_cairo(d->_store_rect)); + IF_SHOW_UNCLEAN( queue_draw() ); + }; - // Handle transitions in and out of decoupled mode + // Handle transitions and actions in response to viewport changes. if (!d->decoupled_mode) { - // Enter decoupled mode if the affine has changed from what was last drawn (assuming there is anything) - if (d->_backing_store && _affine != d->_store_affine) { + // Enter decoupled mode if the affine has changed from what the backing store was drawn at. + if (_affine != d->_store_affine) { + // Copy the backing store to the snapshot, leaving it temporarily in an invalid state. + std::swap(d->_snapshot_store, d->_backing_store); // This will re-use the old snapshot store later if possible. + d->_snapshot_rect = d->_store_rect; + d->_snapshot_affine = d->_store_affine; + d->_snapshot_clean_region = d->_clean_region; + + // Recreate the backing store, making it valid again. + recreate_store(); + + // Enter decoupled mode. + IF_LOGGING( std::cout << "Entering decoupled mode" << std::endl; ) d->decoupled_mode = true; - } - } - else { // if (d->decoupled_mode) - bool exit = false; - if (!d->_backing_store || d->_device_scale != device_scale || d->_store_solid_background != d->solid_background) { - // Exit decoupled mode if the store needs recreating - exit = true; + // Note: If redrawing is fast enough to finish during the frame, then going into decoupled mode, drawing, and leaving + // it again performs exactly the same rendering operations as if we had not gone into it at all. Also, no extra copies + // or blits are performed, and the drawing operations done on the screen are the same. Hence this feature comes at zero cost. } else { - // Also exit if the store has completely gone off the screen - auto pl = Geom::Parallelogram(get_area_world()); - pl *= d->_store_affine * _affine.inverse(); - if (!pl.intersects(d->_store_rect)) { - exit = true; + // Get visible rectangle in canvas coordinates. + const auto visible = Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() ); + if (!d->_store_rect.intersects(visible)) { + // If the store has gone completely off-screen, recreate it. + recreate_store(); + IF_LOGGING( std::cout << "Recreated store" << std::endl; ) } + else if (!d->_store_rect.contains(visible)) + { + // If the store has gone partially off-screen, shift it. + shift_store(); + IF_LOGGING( std::cout << "Shifted store" << std::endl; ) + } + // After these operations, the store should now be fully on-screen. + assert(d->_store_rect.contains(visible)); } - - if (exit) { - d->decoupled_mode = false; - - // Mark store as containing no valid pixels, hence needing recreation - d->_store_rect = empty_int_rect_hack(); - d->_clean_region = Cairo::Region::create(); + } + else { + // Completely cancel the previous redraw and start again if the viewing parameters have changed too much. + auto pl = Geom::Parallelogram(get_area_world()); + pl *= d->_store_affine * _affine.inverse(); + if (!pl.intersects(d->_store_rect)) { + // Store has gone off the screen. + recreate_store(); + IF_LOGGING( std::cout << "Restarting redraw (store off-screen)" << std::endl; ) + } + else { + auto diff = calc_affine_diff(_affine, d->_store_affine); + if (diff > MAX_AFFINE_DIFF) { + // Affine has changed too much. + recreate_store(); + IF_LOGGING( std::cout << "Restarting redraw (affine changed too much)" << std::endl; ) + } } } - // Ensure geometry is up to date - assert(_canvas_item_root); + // Assert that _clean_region is a subregion of _store_rect. + #ifndef NDEBUG + auto tmp = d->_clean_region->copy(); + tmp->subtract(geom_to_cairo(d->_store_rect)); + assert(tmp->empty()); + #endif + + // Ensure the geometry is up-to-date and in the right place. auto affine = d->decoupled_mode ? d->_store_affine : _affine; if (_need_update || d->geom_affine != affine) { + _canvas_item_root->update(affine); d->geom_affine = affine; - _canvas_item_root->update(d->geom_affine); _need_update = false; } - // Assert that _clean_region is a subregion of _store_rect - auto tmp = d->_clean_region->copy(); - tmp->subtract(geom_to_cairo(d->_store_rect)); - assert(tmp->empty()); + // Todo: if redrawing is measured to be extremely heavy, we could consider deferring the start of redrawing until the viewing parameters have changed sufficiently much. + // There are only two main obstructions to implementing this feature. First, having to write timing code to measure whether redraw is heavy. Second, and more importantly, + // it can't be implemented without the Canvas being hooked up to a "transforming finished" event, say mouse release. As these are annoying and the benefit is minor, I'm + // not considering implementing this feature. But it would go here. + // Get the rectangle of store that is visible. + Geom::OptIntRect visible_rect; if (!d->decoupled_mode) { - - // Get window rectangle in canvas coordinates - const auto canvas_rect = Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() ); - - // Ensure store contains canvas_rect - const auto pad = Geom::IntPoint(200, 200); - - if (!d->_backing_store || d->_device_scale != device_scale || d->_store_solid_background != d->solid_background || !d->_store_rect.intersects(canvas_rect)) - { - // Recreate the store, using the same memory if possible - d->_store_rect = Geom::IntRect::from_xywh( _x0, _y0, canvas_rect.width(), canvas_rect.height() ); - d->_store_rect.expandBy(pad); - d->_device_scale = device_scale; - d->_store_solid_background = d->solid_background; - d->_store_affine = _affine; - int desired_width = d->_store_rect.width() * d->_device_scale; - int desired_height = d->_store_rect.height() * d->_device_scale; - if (!d->_backing_store || d->_backing_store->get_width() != desired_width || d->_backing_store->get_height() != desired_height) - d->_backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, desired_width, desired_height); - if (d->solid_background) { - auto cr = Cairo::Context::create(d->_backing_store); - cr->set_source(_background); - cr->set_operator(Cairo::OPERATOR_SOURCE); - cr->paint(); - } - d->_clean_region = Cairo::Region::create(); - IF_SHOW_UNCLEAN( queue_draw() ); - - IF_LOGGING( std::cout << "Recreated store" << std::endl; ) - } - else if (!d->_store_rect.contains(canvas_rect)) - { - // Create new store, copy usable content across, set as new store - auto store_rect = Geom::IntRect::from_xywh( _x0, _y0, canvas_rect.width(), canvas_rect.height() ); - store_rect.expandBy(pad); - auto backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, store_rect.width() * d->_device_scale, store_rect.height() * d->_device_scale); - - auto shift = store_rect.min() - d->_store_rect.min(); - auto reuse_rect = store_rect & d->_store_rect; - assert(reuse_rect); - auto cr = Cairo::Context::create(backing_store); - - // Paint background where necessary - if (d->solid_background) { - auto reg = Cairo::Region::create(geom_to_cairo(store_rect)); - reg->subtract(geom_to_cairo(*reuse_rect)); - reg->translate(-store_rect.left(), -store_rect.top()); - cr->save(); - cr->set_source(_background); - cr->set_operator(Cairo::OPERATOR_SOURCE); - fill_cairo_region(cr, reg); - cr->restore(); - } - - // Copy usuable contents of store shifted - cr->save(); - cr->rectangle(reuse_rect->left() - store_rect.left(), reuse_rect->top() - store_rect.top(), reuse_rect->width(), reuse_rect->height()); - cr->clip(); - cr->set_source(d->_backing_store, -shift.x(), -shift.y()); - cr->set_operator(Cairo::OPERATOR_SOURCE); - cr->paint(); - cr->restore(); - - d->_store_rect = store_rect; - d->_backing_store = std::move(backing_store); - d->_clean_region->intersect(geom_to_cairo(d->_store_rect)); - IF_SHOW_UNCLEAN( queue_draw() ); - - IF_LOGGING( std::cout << "Shifted store" << std::endl; ) - } - - assert(d->_store_rect.contains(canvas_rect)); - } - - // Get region that requires painting - Cairo::RefPtr region; - if (!d->decoupled_mode) { - // Get the window rectangle transformed into canvas space again - const auto canvas_rect = Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() ); - - region = Cairo::Region::create(geom_to_cairo(canvas_rect)); - assert(d->_store_rect.contains(canvas_rect)); // Painting region already guaranteed to lie within store + // By a previous assertion, this always lies within the store. + visible_rect = Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() ); } else { - // Get the window rectangle transformed into canvas space - auto pl = Geom::Parallelogram(Geom::IntRect(0, 0, get_allocation().get_width(), get_allocation().get_height())); - pl *= Geom::Translate(_x0, _y0); + // Get the window rectangle transformed into canvas space. + auto pl = Geom::Parallelogram(Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() )); pl *= d->_store_affine * _affine.inverse(); - // Get bounding box, round outwards + // Get its bounding box, rounded outwards. auto b = pl.bounds(); auto bi = Geom::IntRect(b.min().floor(), b.max().ceil()); - // Set as painting region - region = Cairo::Region::create(geom_to_cairo(bi)); + // The visible rect is the intersection of this with the store + visible_rect = bi & d->_store_rect; - // Todo: In the future, consider coarsening pl into a region rather than taking its bounding box. - // This reduces the area to paint, but increases the number of rectangles, so is NOT necessarily an optimisation, and could be a pessimisation instead. + // Note: We could have used a smaller region containing pl consisting of many rectangles, rather than a single bounding box. + // However, while this uses less pixels it also uses more rectangles, so is NOT necessarily an optimisation and could backfire. + // For this reason, and for simplicity, we use the bounding rect. + } + // The visible rectangle must be a subrectangle of store. + assert(d->_store_rect.contains(visible_rect)); - // Painting region MUST lie within store, so clip it if necessary - region->intersect(geom_to_cairo(d->_store_rect)); + // Get the region to paint, which is the visible rectangle minus the clean region (both subregions of store). + Cairo::RefPtr paint_region; + if (visible_rect) { + paint_region = Cairo::Region::create(geom_to_cairo(*visible_rect)); + paint_region->subtract(d->_clean_region); } - region->subtract(d->_clean_region); + else { + paint_region = Cairo::Region::create(); + } + + // Todo: The SHOW_UNCLEAN debug switch suggests that sometimes the clean region can become highly fragmented, + // ending up as many tiny rectangles. When this happens, it is a disaster for performance, because the render + // cost of a rectangle is not just proportional to its area; there is also a constant part. + // Consider fixing by implementing region compression/coarsening. - // Get mouse position in canvas space + // Get the mouse position in screen space. Geom::IntPoint mouse_loc; if (auto window = get_window()) { int x; int y; Gdk::ModifierType mask; window->get_device_position(Gdk::Display::get_default()->get_default_seat()->get_pointer(), x, y, mask); - mouse_loc = {x, y}; + mouse_loc = Geom::IntPoint(x, y); } else { - mouse_loc = {get_allocation().get_width() / 2, get_allocation().get_height() / 2}; + mouse_loc = Geom::IntPoint(0, 0); // Doesn't particularly matter, just as long as it's initialised. } + // Map the mouse to canvas space. mouse_loc += Geom::IntPoint(_x0, _y0); if (d->decoupled_mode) { mouse_loc = geom_act(d->_store_affine * _affine.inverse(), mouse_loc); } - // Obtain rectangles list sorted by distance from mouse - std::vector rects; - rects.reserve(region->get_num_rectangles()); - for (int i = 0; i < region->get_num_rectangles(); i++) { - rects.emplace_back(cairo_to_geom(region->get_rectangle(i))); - } - std::sort(rects.begin(), rects.end(), [&] (const Geom::IntRect &a, const Geom::IntRect &b) {return distSq(mouse_loc, a) < distSq(mouse_loc, b);}); + // Todo: Consider further subdividing the region if the mouse lies on the edge of a big rectangle. + // Otherwise, the whole of the big rectangle will be rendered first, at the expense of points in other + // rectangles very near to the mouse. - // Set up painting info to pass down + // Obtain the list of rectangles to paint, sorted by distance from mouse. + auto paint_rects = region_to_rects(paint_region); + std::sort(paint_rects.begin(), paint_rects.end(), [&] (const Geom::IntRect &a, const Geom::IntRect &b) { + return distSq(mouse_loc, a) < distSq(mouse_loc, b); + }); + + // Set up painting info to pass down the stack. PaintRectSetup setup; setup.mouse_loc = mouse_loc; - - auto prefs = Inkscape::Preferences::get(); - auto tile_multiplier = prefs->getIntLimited("/options/rendering/tile-multiplier", 16, 1, 512); if (_render_mode != Inkscape::RenderMode::OUTLINE) { // Can't be too small or large gradient will be rerendered too many times! + auto prefs = Inkscape::Preferences::get(); + auto tile_multiplier = prefs->getIntLimited("/options/rendering/tile-multiplier", 16, 1, 512); setup.max_pixels = 65536 * tile_multiplier; } else { // Paths only. 1M is catched buffer and we need four channels. setup.max_pixels = 262144; } - - // Begin painting setup.start_time = g_get_monotonic_time(); - setup.disable_timeouts = _forced_redraw_limit != -1 && _forced_redraw_count >= _forced_redraw_limit; - setup.disable_timeouts = false; // TEMP HACK: I WANT TO INVESTIGATE BEHAVIOUR WITHOUT THIS INTERFERING - - for (const auto &rect : rects) { - //auto area = rect & canvas_rect; // This is strictly not necessary. I am disabling it for now, and will probably remove it completely in a little while. - - /*if (!area || area->hasZeroArea()) { // The same probably goes for this; though it would be worth a check. + //setup.disable_timeouts = _forced_redraw_limit != -1 && _forced_redraw_count >= _forced_redraw_limit; // Enable a forced redraw if asked. + // Todo: Forced redraws are temporarily disabled. They are no longer helpful in all the situations they used to be. + // Need to go through all places where forced redraw is requested, and check whether it is helpful or harmful. + setup.disable_timeouts = false; + + // Paint the rectangles. + for (const auto &rect : paint_rects) { + if (rect.hasZeroArea()) { + // Todo: I'm not sure if these can get through or not. If not, remove this check. This ia a question about cairo. continue; - }*/ - + } if (!paint_rect_internal(setup, rect)) { - // Timed out. Temporarily return to idle loop, and come back here if still idle. - IF_LOGGING( std::cout << "Timed out: " << g_get_monotonic_time() - setup.start_time << " us \n"; ) + // Timed out. Temporarily return to GTK main loop, and come back here when next idle. + // Todo: Logging reveals that Inkscape often underestimates render times, and can sometimes time out quite badly, + // leading to missed frames. Consider fixing the time estimator, possibly based on run-time feedback. + //IF_LOGGING( std::cout << "Timed out: " << g_get_monotonic_time() - setup.start_time << " us" << std::endl; ) IF_FRAMECHECK( f.subtype = 1; ) _forced_redraw_count++; return true; } } - // Check if suppressed a timeout, and adjust counter if so - if (setup.disable_timeouts) - { + // Check if suppressed a timeout. + if (setup.disable_timeouts) { auto now = g_get_monotonic_time(); auto elapsed = now - setup.start_time; - if (elapsed > 1000) { - // Timed out - IF_LOGGING( std::cout << "Ignored timeout: " << g_get_monotonic_time() - setup.start_time << " us \n"; ) + if (elapsed > RENDER_TIME_LIMIT) { + // Timed out. Reset counter. It will count up again on future timeouts, eventually triggering another forced redraw. _forced_redraw_count = 0; - + IF_LOGGING( std::cout << "Ignored timeout: " << g_get_monotonic_time() - setup.start_time << " us" << std::endl; ) IF_FRAMECHECK( f.subtype = 2; ) return false; } } - // Finished drawing - see if we need to exit decoupled mode and do a final redraw + // Finished drawing. If in decoupled mode, see if we need to do a final redraw at the correct affine. if (d->decoupled_mode) { - // Todo: save the current store as a snapshot store to be rendered to back - // do this by swapping the store and the snapshot - - // BTW this is not the right way to do this, and leads to frequent crashes. but it demos the concept. - /*d->_backing_store.clear(); + // Exit decoupled mode. + IF_LOGGING( std::cout << "Finished drawing - exiting decoupled mode" << std::endl; ) d->decoupled_mode = false; - return true;*/ + + // If the affine of the store is not up-to-date with the requested affine, continue the idle process. + if (d->_store_affine != _affine) { + IF_LOGGING( std::cout << "Scheduling final redraw" << std::endl; ) + return true; + } } + // All done, quit the idle process. IF_FRAMECHECK( f.subtype = 3; ) return false; } @@ -1353,9 +1376,9 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th return true; } - #if ENABLE_SLOW_REDRAW + #if ENABLE_OVERBISECTION // Aggressively subdivide into many small rectangles - if (bw > bh || bw > SLOW_REDRAW_SIZE) { + if (bw > bh || bw > OVERBISECTION_SIZE) { int mid = this_rect[Geom::X].middle(); Geom::IntRect lo, hi; @@ -1369,7 +1392,7 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th return paint_rect_internal(setup, hi) && paint_rect_internal(setup, lo); } - } else if (bh > bw && bh > SLOW_REDRAW_SIZE) { + } else if (bh > bw && bh > OVERBISECTION_SIZE) { int mid = this_rect[Geom::Y].middle(); Geom::IntRect lo, hi; @@ -1384,7 +1407,7 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th && paint_rect_internal(setup, lo); } } - #endif // ENABLE_SLOW_REDRAW + #endif // ENABLE_OVERBISECTION if (bw * bh < setup.max_pixels) { // Rectangle is small enough @@ -1441,7 +1464,7 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th if (!setup.disable_timeouts) { auto now = g_get_monotonic_time(); auto elapsed = now - setup.start_time; - if (elapsed > 1000) { + if (elapsed > RENDER_TIME_LIMIT) { return false; } } @@ -1483,7 +1506,7 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th } else { int mid = this_rect[Geom::Y].middle(); - lo = Geom::IntRect(this_rect.left(), this_rect.top(), this_rect.right(), mid ); + lo = Geom::IntRect(this_rect.left(), this_rect.top(), this_rect.right(), mid ); hi = Geom::IntRect(this_rect.left(), mid, this_rect.right(), this_rect.bottom()); if (setup.mouse_loc[Geom::Y] < mid) { -- GitLab From 15168402c3a8ab3f42dffbe6482a29d03a2e52ea Mon Sep 17 00:00:00 2001 From: PBS Date: Fri, 14 Jan 2022 02:01:40 +0900 Subject: [PATCH 10/35] I'm an actual idiot. Did I read the docs? Did I try code completion? Did I think for more than a second? It turns out the setting for turning off anti-aliasing was right there all along. Bang! And the slowness is gone. --- src/ui/widget/canvas.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 5b59c34725..9bf08607f2 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -905,11 +905,8 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->restore(); } else { - // Todo: Cairo does not seem to allow antialiasing for clipping to be disabled. This is completely unwanted, - // but is killing performance, as well as resulting in line artifacts and allowing garbage data to bleed through, - // which we would otherwise be able to guarantee didn't happen. - // Related: https://stackoverflow.com/questions/57390954/why-is-clipping-considered-to-be-too-slow-with-cairo - // Solution: consider bypassing Cairo just for this final compositing step. + // Turn off anti-aliasing for huge performance gains. Only applies to this compositing step. + cr->set_antialias(Cairo::ANTIALIAS_NONE); // Blit background to complement of both clean regions, if solid (and therefore not already drawn). if (d->solid_background) { -- GitLab From 1e5c1d7502318ae2600b57d0fb95b4ac0ca5f778 Mon Sep 17 00:00:00 2001 From: PBS Date: Fri, 14 Jan 2022 21:01:00 +0900 Subject: [PATCH 11/35] Fix the duff frame bug. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There was a visual bug were intensive zoom/rotate would occasionally result in glitchy content appearing for exactly one frame. The glitchy content had the wrong transform and contained uninitialised data. It turned out I was exiting decoupled mode wrong, hence breaking my own design. ━(((;゚Д゚)))━!!! Better fix it quick! 三三ᕕ(ᐛ)ᕗ --- src/ui/widget/canvas.cpp | 61 +++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 9bf08607f2..f5f79c38f9 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -34,8 +34,8 @@ #include "ui/tools/tool-base.h" // Default cursor // Debugging switches -#define ENABLE_FRAMECHECK 1 -#define ENABLE_LOGGING 1 +#define ENABLE_FRAMECHECK 0 +#define ENABLE_LOGGING 0 #define ENABLE_OVERBISECTION 1 #define ENABLE_SLOW_REDRAW 0 #define ENABLE_SHOW_REDRAW 0 @@ -1144,18 +1144,23 @@ Canvas::on_idle() IF_SHOW_UNCLEAN( queue_draw() ); }; + auto take_snapshot = [&, this] { + // Copy the backing store to the snapshot, leaving us temporarily in an invalid state. + std::swap(d->_snapshot_store, d->_backing_store); // This will re-use the old snapshot store later if possible. + d->_snapshot_rect = d->_store_rect; + d->_snapshot_affine = d->_store_affine; + d->_snapshot_clean_region = d->_clean_region->copy(); + + // Recreate the backing store, making the state valid again. + recreate_store(); + }; + // Handle transitions and actions in response to viewport changes. if (!d->decoupled_mode) { // Enter decoupled mode if the affine has changed from what the backing store was drawn at. if (_affine != d->_store_affine) { - // Copy the backing store to the snapshot, leaving it temporarily in an invalid state. - std::swap(d->_snapshot_store, d->_backing_store); // This will re-use the old snapshot store later if possible. - d->_snapshot_rect = d->_store_rect; - d->_snapshot_affine = d->_store_affine; - d->_snapshot_clean_region = d->_clean_region; - - // Recreate the backing store, making it valid again. - recreate_store(); + // Snapshot and reset the backing store. + take_snapshot(); // Enter decoupled mode. IF_LOGGING( std::cout << "Entering decoupled mode" << std::endl; ) @@ -1294,6 +1299,10 @@ Canvas::on_idle() // Set up painting info to pass down the stack. PaintRectSetup setup; setup.mouse_loc = mouse_loc; + // Todo: Logging reveals that Inkscape often underestimates render times, and can sometimes time out quite badly, + // leading to missed frames. Consider fixing the time estimator below, possibly based on run-time feedback. May + // even wish to consider maintaining a coarse heatmap of the drawing to judge rendering time/order. In the meantime, + // this can be worked around be enabling the OVERBISECTION switch with a tile size of about 400. if (_render_mode != Inkscape::RenderMode::OUTLINE) { // Can't be too small or large gradient will be rerendered too many times! auto prefs = Inkscape::Preferences::get(); @@ -1317,8 +1326,6 @@ Canvas::on_idle() } if (!paint_rect_internal(setup, rect)) { // Timed out. Temporarily return to GTK main loop, and come back here when next idle. - // Todo: Logging reveals that Inkscape often underestimates render times, and can sometimes time out quite badly, - // leading to missed frames. Consider fixing the time estimator, possibly based on run-time feedback. //IF_LOGGING( std::cout << "Timed out: " << g_get_monotonic_time() - setup.start_time << " us" << std::endl; ) IF_FRAMECHECK( f.subtype = 1; ) _forced_redraw_count++; @@ -1339,22 +1346,30 @@ Canvas::on_idle() } } - // Finished drawing. If in decoupled mode, see if we need to do a final redraw at the correct affine. + // Finished drawing. See if we need to do a final redraw at the correct affine. if (d->decoupled_mode) { - // Exit decoupled mode. - IF_LOGGING( std::cout << "Finished drawing - exiting decoupled mode" << std::endl; ) - d->decoupled_mode = false; - - // If the affine of the store is not up-to-date with the requested affine, continue the idle process. - if (d->_store_affine != _affine) { + if (d->_store_affine == _affine) { + // Content is rendered at the correct affine - exit decoupled mode and quit idle process. + IF_LOGGING( std::cout << "Finished drawing - exiting decoupled mode" << std::endl; ) + // Exit decoupled mode. + d->decoupled_mode = false; + // Quit idle process. + return false; + } + else { + // Content is rendered at the wrong affine - take a new snapshot and continue idle process to continue rendering at the new affine. IF_LOGGING( std::cout << "Scheduling final redraw" << std::endl; ) + // Snapshot and reset the backing store. + take_snapshot(); + // Continue idle process. return true; } } - - // All done, quit the idle process. - IF_FRAMECHECK( f.subtype = 3; ) - return false; + else { + // All done, quit the idle process. + IF_FRAMECHECK( f.subtype = 3; ) + return false; + } } /* -- GitLab From c5989f6bad6ea7ea4986dd283f312f5e3d6085e9 Mon Sep 17 00:00:00 2001 From: PBS Date: Sat, 15 Jan 2022 03:31:38 +0900 Subject: [PATCH 12/35] And the final ingredient... the COARSENER. This fixes the slow redraw because of fragmentation. Bunch of other really stupid bug fixes went in too. --- src/ui/widget/canvas.cpp | 135 +++++++++++++++++++++++++++++---------- 1 file changed, 103 insertions(+), 32 deletions(-) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index f5f79c38f9..98a4d6386c 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -11,8 +11,9 @@ * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ -#include +#include // Logging #include // Sort +#include // Coarsener #include @@ -36,10 +37,14 @@ // Debugging switches #define ENABLE_FRAMECHECK 0 #define ENABLE_LOGGING 0 -#define ENABLE_OVERBISECTION 1 +#define ENABLE_OVERBISECTION 1 // Current using overbisection at size 400 to work around render time overestimation bug. Without this, expect huge frame drops. #define ENABLE_SLOW_REDRAW 0 #define ENABLE_SHOW_REDRAW 0 #define ENABLE_SHOW_UNCLEAN 0 +#define ENABLE_SHOW_SNAPSHOT 0 + +#define OVERBISECTION_SIZE 400 // 30 // px +#define SLOW_REDRAW_TIME 50 // us #if ENABLE_FRAMECHECK #include "../../../../framecheck.h" @@ -54,14 +59,6 @@ #define IF_LOGGING(X) #endif -#if ENABLE_OVERBISECTION -#define OVERBISECTION_SIZE 400 // 30 // px -#endif - -#if ENABLE_SLOW_REDRAW -#define SLOW_REDRAW_TIME 50 // us -#endif - #if ENABLE_SHOW_UNCLEAN #define IF_SHOW_UNCLEAN(X) X #else @@ -827,7 +824,7 @@ Canvas::on_motion_notify_event(GdkEventMotion *motion_event) auto geom_to_cairo(Geom::IntRect rect) { - return Cairo::RectangleInt { rect.left(), rect.top(), rect.width(), rect.height() }; + return Cairo::RectangleInt{rect.left(), rect.top(), rect.width(), rect.height()}; } auto @@ -941,8 +938,13 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->clip(); cr->set_source(d->_snapshot_store, d->_snapshot_rect.left(), d->_snapshot_rect.top()); cr->set_operator(d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); + cr->paint(); Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST); + #if ENABLE_SHOW_SNAPSHOT + cr->set_source_rgba(0, 0, 1, 0.2); + cr->set_operator(Cairo::OPERATOR_OVER); cr->paint(); + #endif cr->restore(); // Draw transformed store, clipped to clean region. @@ -1018,30 +1020,89 @@ distSq(const Geom::IntPoint pt, const Geom::IntRect &rect) return v.x() * v.x() + v.y() * v.y(); } -auto -empty_int_rect_hack() -{ - auto min = std::numeric_limits::min(); - auto interval = Geom::GenericInterval(min, min); - return Geom::IntRect(interval, interval); -} - auto calc_affine_diff(const Geom::Affine &a, const Geom::Affine &b) { auto c = a.inverse() * b; return std::abs(c[0] - 1) + std::abs(c[1]) + std::abs(c[2]) + std::abs(c[3] - 1); } +// Replace a region with a larger region consisting of fewer, larger rectangles. (Allowed to slightly overlap.) auto -region_to_rects(const Cairo::RefPtr ®ion) +coarsen(const Cairo::RefPtr ®ion) { - std::vector rects; + IF_FRAMECHECK(framecheck_whole_function); + + // Todo: Possibly further tune these thresholds. + constexpr int MIN_SIZE = 200; + constexpr int GLUE_SIZE = 80; + + // Sort the rects by minExtent + struct Compare + { + bool operator()(const Geom::IntRect &a, const Geom::IntRect &b) const { + return a.minExtent() < b.minExtent(); + } + }; + std::multiset rects; int nrects = region->get_num_rectangles(); - rects.reserve(nrects); for (int i = 0; i < nrects; i++) { - rects.emplace_back(cairo_to_geom(region->get_rectangle(i))); + rects.emplace(cairo_to_geom(region->get_rectangle(i))); + } + + // List of processed rectangles. + std::vector processed; + processed.reserve(nrects); + + // Repeatedly expand small rectangles by absorbing their nearby small rectangles. + while (!rects.empty() && rects.begin()->minExtent() < MIN_SIZE) { + // Extract the smallest unprocessed rectangle. + auto rect = *rects.begin(); + rects.erase(rects.begin()); + + while (true) { + // Find the glue zone. + auto glue_zone = rect; + glue_zone.expandBy(GLUE_SIZE); + + // Absorb rectangles in the glue zone. We could make this a lot faster, but it's already fast enough. + auto orig = rect; + for (auto it = rects.begin(); it != rects.end(); ) { + if (glue_zone.contains(*it)) { + rect.unionWith(*it); + it = rects.erase(it); + } + else { + ++it; + } + } + for (auto it = processed.begin(); it != processed.end(); ) { + if (glue_zone.contains(*it)) { + rect.unionWith(*it); + *it = processed.back(); + processed.pop_back(); + } + else { + ++it; + } + } + + // Stop growing if not changed or now big enough. + bool finished = rect == orig || rect.minExtent() >= MIN_SIZE; + if (finished) { + break; + } + } + + // Put the finished rectangle in processed. + processed.emplace_back(rect); + } + + // Put any remaining rectangles in processed. + for (auto &rect : rects) { + processed.emplace_back(rect); } - return rects; + + return processed; } bool @@ -1247,7 +1308,7 @@ Canvas::on_idle() // Note: We could have used a smaller region containing pl consisting of many rectangles, rather than a single bounding box. // However, while this uses less pixels it also uses more rectangles, so is NOT necessarily an optimisation and could backfire. - // For this reason, and for simplicity, we use the bounding rect. + // For this reason, and for simplicity, we use the bounding rect. (Note: Slightly invalidated by addition of coarsener below.) } // The visible rectangle must be a subrectangle of store. assert(d->_store_rect.contains(visible_rect)); @@ -1262,10 +1323,21 @@ Canvas::on_idle() paint_region = Cairo::Region::create(); } - // Todo: The SHOW_UNCLEAN debug switch suggests that sometimes the clean region can become highly fragmented, - // ending up as many tiny rectangles. When this happens, it is a disaster for performance, because the render - // cost of a rectangle is not just proportional to its area; there is also a constant part. - // Consider fixing by implementing region compression/coarsening. + // Get the list of rectangles to paint, coarsened to avoid fragmentation. + auto paint_rects = coarsen(paint_region); + + // Clip the coarsened rectangles back to the visible rect, in case coarsening made them go outside it. Otherwise, we could render to outside the store! + for (auto it = paint_rects.begin(); it != paint_rects.end(); ) { + auto opt = *it & *visible_rect; + if (opt) { + *it = *opt; + ++it; + } + else { + *it = paint_rects.back(); + paint_rects.pop_back(); + } + } // Get the mouse position in screen space. Geom::IntPoint mouse_loc; @@ -1290,8 +1362,7 @@ Canvas::on_idle() // Otherwise, the whole of the big rectangle will be rendered first, at the expense of points in other // rectangles very near to the mouse. - // Obtain the list of rectangles to paint, sorted by distance from mouse. - auto paint_rects = region_to_rects(paint_region); + // Sort the rectangles to paint, sorted by distance from mouse. std::sort(paint_rects.begin(), paint_rects.end(), [&] (const Geom::IntRect &a, const Geom::IntRect &b) { return distSq(mouse_loc, a) < distSq(mouse_loc, b); }); @@ -1390,7 +1461,7 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th #if ENABLE_OVERBISECTION // Aggressively subdivide into many small rectangles - if (bw > bh || bw > OVERBISECTION_SIZE) { + if (bw > bh && bw > OVERBISECTION_SIZE) { int mid = this_rect[Geom::X].middle(); Geom::IntRect lo, hi; -- GitLab From e36277d81eaec797945219bb004ef2c9962efa5b Mon Sep 17 00:00:00 2001 From: PBS Date: Sat, 15 Jan 2022 05:03:15 +0900 Subject: [PATCH 13/35] Coding style guidelines. --- src/desktop.cpp | 2 +- src/ui/widget/canvas.cpp | 50 +++++++++++++++++++++------------------- src/ui/widget/canvas.h | 4 ++-- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/desktop.cpp b/src/desktop.cpp index 0e3533c2c7..e8d82683af 100644 --- a/src/desktop.cpp +++ b/src/desktop.cpp @@ -776,7 +776,7 @@ SPDesktop::zoom_selection() } Geom::Point SPDesktop::current_center() const { - return Geom::Point(canvas->get_area_world().midpoint()) * _current_affine.w2d(); + return Geom::Rect(canvas->get_area_world()).midpoint() * _current_affine.w2d(); } /** diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 98a4d6386c..f88d9e18fc 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -37,7 +37,7 @@ // Debugging switches #define ENABLE_FRAMECHECK 0 #define ENABLE_LOGGING 0 -#define ENABLE_OVERBISECTION 1 // Current using overbisection at size 400 to work around render time overestimation bug. Without this, expect huge frame drops. +#define ENABLE_OVERBISECTION 1 // Currently using overbisection at size 400 to work around the render time overestimation bug. Without this, expect huge frame drops. #define ENABLE_SLOW_REDRAW 0 #define ENABLE_SHOW_REDRAW 0 #define ENABLE_SHOW_UNCLEAN 0 @@ -132,10 +132,11 @@ struct PaintRectSetup { class CanvasPrivate { -private: +public: friend class Canvas; Canvas *q; + CanvasPrivate(Canvas *q) : q(q) {} // Important global properties of all the stores. If these change, all the stores must be recreated. int _device_scale = 1; @@ -178,13 +179,11 @@ void CanvasPrivate::schedule_bucket_emptier() { if (bucket_emptier.connected()) return; - bucket_emptier = Glib::signal_idle().connect([this] - { + bucket_emptier = Glib::signal_idle().connect([this] { bucket_emptier.disconnect(); empty_bucket(); return false; - } - , G_PRIORITY_DEFAULT_IDLE - 5); // before lowpri_idle + }, G_PRIORITY_DEFAULT_IDLE - 5); // before lowpri_idle } void CanvasPrivate::empty_bucket() @@ -193,8 +192,7 @@ void CanvasPrivate::empty_bucket() auto bucket2 = std::move(bucket); - for (auto &event : bucket2) - { + for (auto &event : bucket2) { // Block undo/redo while anything is dragged. if (event->type == GDK_BUTTON_PRESS && event->button.button == 1) { q->_is_dragging = true; @@ -205,7 +203,7 @@ void CanvasPrivate::empty_bucket() bool finished = false; if (q->_current_canvas_item) { - // Choose where to send event + // Choose where to send event. CanvasItem *item = q->_current_canvas_item; if (q->_grabbed_canvas_item && !q->_current_canvas_item->is_descendant_of(q->_grabbed_canvas_item)) { @@ -220,9 +218,8 @@ void CanvasPrivate::empty_bucket() } } - if (!finished) - { - // Re-fire the event at the window, and ignore it when it comes back here again + if (!finished) { + // Re-fire the event at the window, and ignore it when it comes back here again. ignore = event.get(); q->get_toplevel()->event(event.get()); ignore = nullptr; @@ -236,7 +233,7 @@ void CanvasPrivate::queue_draw_area(Geom::IntRect &rect) } Canvas::Canvas() - : _size_observer(this, "/options/grabsize/value"), d(std::make_unique()) + : _size_observer(this, "/options/grabsize/value"), d(std::make_unique(this)) { set_name("InkscapeCanvas"); @@ -270,8 +267,6 @@ Canvas::Canvas() #if ENABLE_SHOW_REDRAW srand(g_get_monotonic_time()); #endif - - d->q = this; } Canvas::~Canvas() @@ -882,8 +877,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) assert(_drawing); // Blit background if not solid. (If solid, it is baked into the stores.) - if (!d->solid_background) - { + if (!d->solid_background) { IF_FRAMECHECK( f = Prof("background"); ) cr->save(); cr->set_operator(Cairo::OPERATOR_SOURCE); @@ -977,13 +971,14 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->restore(); #endif - if (d->pending_draw) - { - if (!d->bucket.empty()) d->schedule_bucket_emptier(); + if (d->pending_draw) { + if (!d->bucket.empty()) { + d->schedule_bucket_emptier(); + } d->pending_draw = false; } - // Todo: Add back X-ray view. (Should be easy.) + // Todo: Add back X-ray view. return true; } @@ -1789,8 +1784,8 @@ Canvas::pick_current_item(GdkEvent *event) // Ensure geometry is correct. auto affine = d->decoupled_mode ? d->_store_affine : _affine; if (_need_update || d->geom_affine != affine) { + _canvas_item_root->update(affine); d->geom_affine = affine; - _canvas_item_root->update(d->geom_affine); _need_update = false; } @@ -1940,7 +1935,9 @@ Canvas::emit_event(GdkEvent *event) { IF_FRAMECHECK(framecheck_whole_function) - if (event == d->ignore) return false; + if (event == d->ignore) { + return false; + } Gdk::EventMask mask = (Gdk::EventMask)0; if (_grabbed_canvas_item) { @@ -2003,7 +2000,12 @@ Canvas::emit_event(GdkEvent *event) } d->bucket.emplace_back(event_copy); - if (!d->pending_draw) add_tick_callback([this] (const Glib::RefPtr&) {d->schedule_bucket_emptier(); return false;}); + if (!d->pending_draw) { + add_tick_callback([this] (const Glib::RefPtr&) { + d->schedule_bucket_emptier(); + return false; + }); + } return true; } diff --git a/src/ui/widget/canvas.h b/src/ui/widget/canvas.h index 8114842da1..791685cbb1 100644 --- a/src/ui/widget/canvas.h +++ b/src/ui/widget/canvas.h @@ -155,9 +155,9 @@ private: // Painting // In order they are called in painting. - bool paint(); bool paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &this_rect); void paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtr &store); + void add_clippath(const Cairo::RefPtr& cr); void set_cursor(); @@ -175,7 +175,7 @@ private: // Geometry int _x0 = 0, _y0 = 0; ///< Coordinates of top-left pixel of canvas view within canvas. - Geom::Affine _affine; // Only used for canvas items at the moment. + Geom::Affine _affine; ///< The affine that we have been requested to draw at. // Event handling/item picking GdkEvent _pick_event; ///< Event used to find currently selected item. -- GitLab From 0a8d3bc9254742610dc51ca5b76c39eebd1f46b8 Mon Sep 17 00:00:00 2001 From: PBS Date: Sun, 16 Jan 2022 16:28:06 +0900 Subject: [PATCH 14/35] Expose all tunables to gui. Fix dumb bisector bug. --- src/ui/dialog/inkscape-preferences.cpp | 62 ++- src/ui/dialog/inkscape-preferences.h | 32 +- src/ui/widget/canvas.cpp | 583 ++++++++++++++----------- 3 files changed, 382 insertions(+), 295 deletions(-) diff --git a/src/ui/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp index 5f3da10f1b..9135630aee 100644 --- a/src/ui/dialog/inkscape-preferences.cpp +++ b/src/ui/dialog/inkscape-preferences.cpp @@ -2627,11 +2627,9 @@ void InkscapePreferences::initPageBehavior() 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, 8.0, 1.0, 2.0, 4.0, true, false); - _page_rendering.add_line( false, _("Number of _Threads:"), _filter_multi_threaded, _("(requires restart)"), - _("Configure number of processors/threads to use when rendering filters"), false); + _page_rendering.add_line( false, _("Number of _Threads:"), _filter_multi_threaded, _("(requires restart)"), _("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); @@ -2639,32 +2637,16 @@ void InkscapePreferences::initPageRendering() // 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); + _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 xray 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); + _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); // rendering outline overlay opcaity _rendering_outline_overlay_opacity.init("/options/rendering/outline-overlay-opacity", 1.0, 100.0, 1.0, 5.0, 50.0, true, false); _rendering_outline_overlay_opacity.signal_focus_out_event().connect(sigc::mem_fun(*this, &InkscapePreferences::on_outline_overlay_changed)); - _page_rendering.add_line( false, _("Outline overlay opacity:"), _rendering_outline_overlay_opacity, _("%"), - _("Opacity of the color in outline overlay view mode"), false); - - { - // if these GTK constants ever change, consider adding a compatibility shim to SPCanvas::addIdle() - static_assert(G_PRIORITY_HIGH_IDLE == 100, "G_PRIORITY_HIGH_IDLE must be 100 to match preferences.xml"); - static_assert(G_PRIORITY_DEFAULT_IDLE == 200, "G_PRIORITY_DEFAULT_IDLE must be 200 to match preferences.xml"); - - Glib::ustring redrawPriorityLabels[] = {_("Responsive"), _("Conservative")}; - int redrawPriorityValues[] = {G_PRIORITY_HIGH_IDLE, G_PRIORITY_DEFAULT_IDLE}; - - // redraw priority - _rendering_redraw_priority.init("/options/redrawpriority/value", redrawPriorityLabels, redrawPriorityValues, G_N_ELEMENTS(redrawPriorityLabels), 0); - _page_rendering.add_line(false, _("Redraw while editing:"), _rendering_redraw_priority, "", - _("Set how quickly the canvas display is updated while editing objects"), false); - } + _page_rendering.add_line( false, _("Outline overlay opacity:"), _rendering_outline_overlay_opacity, _("%"), _("Opacity of the color in outline overlay view mode"), false); /* blur quality */ _blur_quality_best.init ( _("Best quality (slowest)"), "/options/blurquality/value", @@ -2714,6 +2696,40 @@ void InkscapePreferences::initPageRendering() _page_rendering.add_line( true, "", _filter_quality_worst, "", _("Lowest quality (considerable artifacts), but display is fastest")); + _page_rendering.add_group_header(_("Debugging, profiling, and experiments")); + _canvas_debug_framecheck.init(_("Framecheck"), "/options/rendering/debug/framecheck", false); + _page_rendering.add_line(true, "", _canvas_debug_framecheck, "", _("Print profiling data of selected operations to a file")); + _canvas_debug_logging.init(_("Logging"), "/options/rendering/debug/logging", false); + _page_rendering.add_line(true, "", _canvas_debug_logging, "", _("Log certain events to the console")); + _canvas_debug_overbisection.init(_("Overbisection"), "/options/rendering/debug/overbisection", false); + _page_rendering.add_line(true, "", _canvas_debug_overbisection, "", _("Bisect all tiles until they reach a minimum size")); + _canvas_debug_overbisection_size.init("/options/rendering/debug/overbisection_size", 1.0, 10000.0, 1.0, 0.0, 30.0, true, false); + _page_rendering.add_line(true, _("Overbisection size"), _canvas_debug_overbisection_size, C_("pixel abbreviation", "px"), _("The maxmimum allowed tile size"), false); + _canvas_debug_slow_redraw.init(_("Slow redraw"), "/options/rendering/debug/slow_redraw", false); + _page_rendering.add_line(true, "", _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); + _page_rendering.add_line(true, _("Slow redraw time"), _canvas_debug_slow_redraw_time, C_("microsecond abbreviation", "μs"), _("The delay to introduce for each tile"), false); + _canvas_debug_show_redraw.init(_("Show redraw"), "/options/rendering/debug/show_redraw", false); + _page_rendering.add_line(true, "", _canvas_debug_show_redraw, "", _("Paint a translucent random colour over each newly drawn tile")); + _canvas_debug_show_unclean.init(_("Show unclean region"), "/options/rendering/debug/show_unclean", false); + _page_rendering.add_line(true, "", _canvas_debug_show_unclean, "", _("Show the unclean region in red")); + _canvas_debug_show_snapshot.init(_("Show snapshot"), "/options/rendering/debug/show_snapshot", false); + _page_rendering.add_line(true, "", _canvas_debug_show_snapshot, "", _("Show the snapshot region in blue")); + _canvas_debug_sticky_decoupled.init(_("Sticky decoupled mode"), "/options/rendering/debug/sticky_decoupled", false); + _page_rendering.add_line(true, "", _canvas_debug_sticky_decoupled, "", _("Stay in decoupled mode even after rendering is complete")); + + _page_rendering.add_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); + _page_rendering.add_line(true, _("Render time limit"), _canvas_render_time_limit, C_("microsecond abbreviation", "μs"), _("The maximum time allowed for a rendering time slice"), false); + _canvas_max_affine_diff.init("/options/rendering/max_affine_diff", 0.0, 100.0, 0.1, 0.0, 1.0, false, false); + _page_rendering.add_line(true, _("Max affine diff"), _canvas_max_affine_diff, "", _("How much the viewing transformation can change before throwing away the current redraw and starting again"), false); + _canvas_pad.init("/options/rendering/pad", 0.0, 1000.0, 1.0, 0.0, 200.0, true, false); + _page_rendering.add_line(true, _("Buffer padding"), _canvas_pad, C_("pixel abbreviation", "px"), _("Use buffers bigger than the window by this amount"), false); + _canvas_coarsener_min_size.init("/options/rendering/coarsener_min_size", 0.0, 1000.0, 1.0, 0.0, 200.0, true, false); + _page_rendering.add_line(true, _("Coarsener min size"), _canvas_coarsener_min_size, C_("pixel abbreviation", "px"), _("Parameter given to the coarsener algorithm when applied to the paint region. Probably best left alone!"), false); + _canvas_coarsener_glue_size.init("/options/rendering/coarsener_glue_size", 0.0, 1000.0, 1.0, 0.0, 80.0, true, false); + _page_rendering.add_line(true, _("Coarsener glue size"), _canvas_coarsener_glue_size, C_("pixel abbreviation", "px"), _("Parameter given to the coarsener algorithm when applied to the paint region. Probably best left alone!"), false); + this->AddPage(_page_rendering, _("Rendering"), PREFS_PAGE_RENDERING); } diff --git a/src/ui/dialog/inkscape-preferences.h b/src/ui/dialog/inkscape-preferences.h index ca1756e465..41352ea521 100644 --- a/src/ui/dialog/inkscape-preferences.h +++ b/src/ui/dialog/inkscape-preferences.h @@ -320,6 +320,11 @@ protected: UI::Widget::PrefRadioButton _mask_grouping_all; UI::Widget::PrefCheckButton _mask_ungrouping; + 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::PrefRadioButton _blur_quality_best; UI::Widget::PrefRadioButton _blur_quality_better; UI::Widget::PrefRadioButton _blur_quality_normal; @@ -330,14 +335,23 @@ protected: UI::Widget::PrefRadioButton _filter_quality_normal; UI::Widget::PrefRadioButton _filter_quality_worse; UI::Widget::PrefRadioButton _filter_quality_worst; - UI::Widget::PrefCheckButton _show_filters_info_box; - UI::Widget::PrefCheckButton _rendering_image_outline; - 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 _rendering_redraw_priority; - UI::Widget::PrefSpinButton _filter_multi_threaded; + + UI::Widget::PrefCheckButton _canvas_debug_framecheck; + UI::Widget::PrefCheckButton _canvas_debug_logging; + UI::Widget::PrefCheckButton _canvas_debug_overbisection; + UI::Widget::PrefCheckButton _canvas_debug_slow_redraw; + UI::Widget::PrefCheckButton _canvas_debug_show_redraw; + UI::Widget::PrefCheckButton _canvas_debug_show_unclean; + UI::Widget::PrefCheckButton _canvas_debug_show_snapshot; + UI::Widget::PrefCheckButton _canvas_debug_sticky_decoupled; + UI::Widget::PrefSpinButton _canvas_debug_overbisection_size; + UI::Widget::PrefSpinButton _canvas_debug_slow_redraw_time; + + UI::Widget::PrefSpinButton _canvas_render_time_limit; + UI::Widget::PrefSpinButton _canvas_max_affine_diff; + UI::Widget::PrefSpinButton _canvas_pad; + UI::Widget::PrefSpinButton _canvas_coarsener_min_size; + UI::Widget::PrefSpinButton _canvas_coarsener_glue_size; UI::Widget::PrefCheckButton _trans_scale_stroke; UI::Widget::PrefCheckButton _trans_scale_corner; @@ -367,6 +381,7 @@ protected: UI::Widget::PrefSpinButton _importexport_export_res; UI::Widget::PrefSpinButton _importexport_import_res; UI::Widget::PrefCheckButton _importexport_import_res_override; + UI::Widget::PrefCheckButton _rendering_image_outline; UI::Widget::PrefSlider _snap_delay; UI::Widget::PrefSlider _snap_weight; UI::Widget::PrefSlider _snap_persistence; @@ -417,6 +432,7 @@ protected: UI::Widget::PrefCheckButton _ui_realworldzoom; UI::Widget::PrefCheckButton _ui_partialdynamic; UI::Widget::ZoomCorrRulerSlider _ui_zoom_correction; + UI::Widget::PrefCheckButton _show_filters_info_box; UI::Widget::PrefCheckButton _ui_yaxisdown; UI::Widget::PrefCheckButton _ui_rotationlock; UI::Widget::PrefCheckButton _ui_cursorscaling; diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index f88d9e18fc..cc35449fe7 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -34,36 +34,7 @@ #include "ui/tools/tool-base.h" // Default cursor -// Debugging switches -#define ENABLE_FRAMECHECK 0 -#define ENABLE_LOGGING 0 -#define ENABLE_OVERBISECTION 1 // Currently using overbisection at size 400 to work around the render time overestimation bug. Without this, expect huge frame drops. -#define ENABLE_SLOW_REDRAW 0 -#define ENABLE_SHOW_REDRAW 0 -#define ENABLE_SHOW_UNCLEAN 0 -#define ENABLE_SHOW_SNAPSHOT 0 - -#define OVERBISECTION_SIZE 400 // 30 // px -#define SLOW_REDRAW_TIME 50 // us - -#if ENABLE_FRAMECHECK -#include "../../../../framecheck.h" -#define IF_FRAMECHECK(X) X -#else -#define IF_FRAMECHECK(X) -#endif - -#if ENABLE_LOGGING -#define IF_LOGGING(X) X -#else -#define IF_LOGGING(X) -#endif - -#if ENABLE_SHOW_UNCLEAN -#define IF_SHOW_UNCLEAN(X) X -#else -#define IF_SHOW_UNCLEAN(X) -#endif +#include "framecheck.h" // For frame profiling /* * The canvas is responsible for rendering the SVG drawing with various "control" @@ -120,9 +91,6 @@ namespace Inkscape { namespace UI { namespace Widget { -static constexpr int RENDER_TIME_LIMIT = 1000; // Render time limit, in microseconds. -static constexpr double MAX_AFFINE_DIFF = 1.0; // Threshold for redraw cancel and restart. - struct PaintRectSetup { Geom::IntPoint mouse_loc; int max_pixels; @@ -130,6 +98,113 @@ struct PaintRectSetup { bool disable_timeouts; }; +// Preferences system + +template +struct PrefBase +{ + T t; + std::unique_ptr obs; + operator T() const {return t;} +}; + +template +struct Pref {}; + +template<> +struct Pref : PrefBase +{ + Pref(const char *path) + { + auto prefs = Inkscape::Preferences::get(); + t = prefs->getBool(path); + obs = prefs->createObserver(path, [this] (const Preferences::Entry &e) {t = e.getBool();}); + } +}; + +template<> +struct Pref : PrefBase +{ + Pref(const char *path, int def, int min, int max) + { + auto prefs = Inkscape::Preferences::get(); + t = prefs->getIntLimited(path, def, min, max); + obs = prefs->createObserver(path, [&, this] (const Preferences::Entry &e) {t = e.getIntLimited(def, min, max);}); + } +}; + +template<> +struct Pref : PrefBase +{ + Pref(const char *path, double def, double min, double max) + { + auto prefs = Inkscape::Preferences::get(); + t = prefs->getDoubleLimited(path, def, min, max); + obs = prefs->createObserver(path, [&, this] (const Preferences::Entry &e) {t = e.getDoubleLimited(def, min, max);}); + } +}; + +// Note: See inkscape-preferences.cpp or the Preferences GUI for documentation on the meaning of these parameters. +struct Prefs +{ + struct Debug + { + Debug() + : framecheck("/options/rendering/debug/framecheck") + , logging("/options/rendering/debug/logging") + , overbisection("/options/rendering/debug/overbisection") + , slow_redraw("/options/rendering/debug/slow_redraw") + , show_redraw("/options/rendering/debug/show_redraw") + , show_unclean("/options/rendering/debug/show_unclean") + , show_snapshot("/options/rendering/debug/show_snapshot") + , sticky_decoupled("/options/rendering/debug/sticky_decoupled") + , overbisection_size("/options/rendering/debug/overbisection_size", 30, 1, 10000) + , slow_redraw_time("/options/rendering/debug/slow_redraw_time", 50, 0, 1000000) + {} + + // Debug switches + Pref framecheck; // Print per-frame profiling data of selected functions to a file. + Pref logging; // Log certain events to stdout. + Pref overbisection; // Bisect all tiles until they reach a minimum size. + Pref slow_redraw; // Introduce a fixed delay for each tile. + Pref show_redraw; // Paint a translucent random colour over each newly drawn tile. + Pref show_unclean; // Paint the unclean region in red. + Pref show_snapshot; // Paint the snapshot region in blue. + Pref sticky_decoupled; // Stay in decoupled mode even after rendering is completed. + + // Debug switch parameters + Pref overbisection_size; // The maxmimum allowed tile size, in pixels. + Pref slow_redraw_time; // The delay to introduce for each tile, in microseconds. + }; + + Prefs() + : tile_size("/options/rendering/tile-size", 16, 1, 10000) + , tile_multiplier("/options/rendering/tile-multiplier", 16, 1, 512) + , x_ray_radius("/options/rendering/xray-radius", 100, 1, 1500) + , from_display("/options/displayprofile/from_display") + , render_time_limit("/options/rendering/render_time_limit", 1000, 100, 1000000) + , max_affine_diff("/options/rendering/max_affine_diff", 1.0, 0.0, 100.0) + , pad("/options/rendering/pad", 200, 0, 1000) + , coarsener_min_size("/options/rendering/coarsener_min_size", 200, 0, 1000) + , coarsener_glue_size("/options/rendering/coarsener_glue_size", 80, 0, 1000) + {} + + Debug debug; + + // Original parameters + Pref tile_size; + Pref tile_multiplier; + Pref x_ray_radius; + Pref from_display; + + // New parameters + Pref render_time_limit; // The maximum time allowed for a rendering time slice (on_idle) before we return to the main loop, in microseconds. + Pref max_affine_diff; // How much the viewing transformation can change before we decide to throw away the current redraw and start again. + Pref pad; // Use buffers bigger than the window by this amount so that scrolling becomes a no-op most of the time. + Pref coarsener_min_size; // Parameters given to the coarsener algorithm when used to coarsen the paint region. You probably don't want to change these! + Pref coarsener_glue_size; // (the same) +}; + class CanvasPrivate { public: @@ -173,6 +248,8 @@ public: Geom::Affine geom_affine; void queue_draw_area(Geom::IntRect &rect); + + Prefs prefs; }; void CanvasPrivate::schedule_bucket_emptier() @@ -264,9 +341,7 @@ Canvas::Canvas() _canvas_item_root->set_name("CanvasItemGroup:Root"); _canvas_item_root->set_canvas(this); - #if ENABLE_SHOW_REDRAW - srand(g_get_monotonic_time()); - #endif + if (d->prefs.debug.show_redraw) srand(g_get_monotonic_time()); // Initialise seed for random colours. } Canvas::~Canvas() @@ -339,7 +414,7 @@ Canvas::redraw_all() } d->_clean_region = Cairo::Region::create(); // Empty region (i.e. everything is dirty). add_idle(); - IF_SHOW_UNCLEAN( queue_draw() ); + if (d->prefs.debug.show_unclean) queue_draw(); } /** @@ -372,7 +447,7 @@ Canvas::redraw_area(int x0, int y0, int x1, int y1) Cairo::RectangleInt crect = { x0, y0, x1-x0, y1-y0 }; d->_clean_region->subtract(crect); add_idle(); - IF_SHOW_UNCLEAN( queue_draw() ); + if (d->prefs.debug.show_unclean) queue_draw(); } void @@ -873,7 +948,14 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) // sp_canvas_item_recursive_print_tree(0, _root); // canvas_item_print_tree(_canvas_item_root); - assert(d->_backing_store); + // Todo: On MacOS, GTK event loop ordering problems result in this function being called before the first call to hipri_idle, which is technically + // impossible because hipri_idle has higher priority than draw. When this is fixed, the following can be made back into an assertion. + if (!d->_backing_store) { + std::cout << "CRASH AVERTED: on_draw called before first call to on_idle!" << std::endl; + // Should really paint the background, but not worth it. + return true; + } + assert(_drawing); // Blit background if not solid. (If solid, it is baked into the stores.) @@ -894,8 +976,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->set_source(d->_backing_store, d->_store_rect.left() - _x0, d->_store_rect.top() - _y0); cr->paint(); cr->restore(); - } - else { + } else { // Turn off anti-aliasing for huge performance gains. Only applies to this compositing step. cr->set_antialias(Cairo::ANTIALIAS_NONE); @@ -934,11 +1015,11 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->set_operator(d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); cr->paint(); Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST); - #if ENABLE_SHOW_SNAPSHOT - cr->set_source_rgba(0, 0, 1, 0.2); - cr->set_operator(Cairo::OPERATOR_OVER); - cr->paint(); - #endif + if (d->prefs.debug.show_snapshot) { + cr->set_source_rgba(0, 0, 1, 0.2); + cr->set_operator(Cairo::OPERATOR_OVER); + cr->paint(); + } cr->restore(); // Draw transformed store, clipped to clean region. @@ -955,22 +1036,23 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->restore(); } - #if ENABLE_SHOW_UNCLEAN - IF_FRAMECHECK( f = Prof("paint_unclean"); ) // Paint unclean regions in red. - auto reg = Cairo::Region::create(geom_to_cairo(d->_store_rect)); - reg->subtract(d->_clean_region); - cr->save(); - cr->translate(-_x0, -_y0); - if (d->decoupled_mode) { - cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); + if (d->prefs.debug.show_unclean) { + IF_FRAMECHECK( f = Prof("paint_unclean"); ) + auto reg = Cairo::Region::create(geom_to_cairo(d->_store_rect)); + reg->subtract(d->_clean_region); + cr->save(); + cr->translate(-_x0, -_y0); + if (d->decoupled_mode) { + cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); + } + cr->set_source_rgba(1, 0, 0, 0.2); + region_to_path(cr, reg); + cr->fill(); + cr->restore(); } - cr->set_source_rgba(1, 0, 0, 0.2); - region_to_path(cr, reg); - cr->fill(); - cr->restore(); - #endif + // Process bucketed events as soon as possible after draw. if (d->pending_draw) { if (!d->bucket.empty()) { d->schedule_bucket_emptier(); @@ -1023,14 +1105,10 @@ calc_affine_diff(const Geom::Affine &a, const Geom::Affine &b) { // Replace a region with a larger region consisting of fewer, larger rectangles. (Allowed to slightly overlap.) auto -coarsen(const Cairo::RefPtr ®ion) +coarsen(const Cairo::RefPtr ®ion, int min_size, int glue_size) { IF_FRAMECHECK(framecheck_whole_function); - // Todo: Possibly further tune these thresholds. - constexpr int MIN_SIZE = 200; - constexpr int GLUE_SIZE = 80; - // Sort the rects by minExtent struct Compare { @@ -1049,7 +1127,7 @@ coarsen(const Cairo::RefPtr ®ion) processed.reserve(nrects); // Repeatedly expand small rectangles by absorbing their nearby small rectangles. - while (!rects.empty() && rects.begin()->minExtent() < MIN_SIZE) { + while (!rects.empty() && rects.begin()->minExtent() < min_size) { // Extract the smallest unprocessed rectangle. auto rect = *rects.begin(); rects.erase(rects.begin()); @@ -1057,7 +1135,7 @@ coarsen(const Cairo::RefPtr ®ion) while (true) { // Find the glue zone. auto glue_zone = rect; - glue_zone.expandBy(GLUE_SIZE); + glue_zone.expandBy(glue_size); // Absorb rectangles in the glue zone. We could make this a lot faster, but it's already fast enough. auto orig = rect; @@ -1065,8 +1143,7 @@ coarsen(const Cairo::RefPtr ®ion) if (glue_zone.contains(*it)) { rect.unionWith(*it); it = rects.erase(it); - } - else { + } else { ++it; } } @@ -1075,14 +1152,13 @@ coarsen(const Cairo::RefPtr ®ion) rect.unionWith(*it); *it = processed.back(); processed.pop_back(); - } - else { + } else { ++it; } } // Stop growing if not changed or now big enough. - bool finished = rect == orig || rect.minExtent() >= MIN_SIZE; + bool finished = rect == orig || rect.minExtent() >= min_size; if (finished) { break; } @@ -1117,7 +1193,7 @@ Canvas::on_idle() return false; } - const Geom::IntPoint pad(200, 200); // Todo: Tune. + const Geom::IntPoint pad(d->prefs.pad, d->prefs.pad); auto recreate_store = [&, this] { // Recreate the store at the current affine so that it covers the visible region. d->_store_rect = Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() ); @@ -1133,13 +1209,12 @@ Canvas::on_idle() if (d->solid_background) { cr->set_operator(Cairo::OPERATOR_SOURCE); cr->set_source(_background); - } - else { + } else { cr->set_operator(Cairo::OPERATOR_CLEAR); } cr->paint(); d->_clean_region = Cairo::Region::create(); - IF_SHOW_UNCLEAN( queue_draw() ); + if (d->prefs.debug.show_unclean) queue_draw(); }; // Determine the rendering parameters have changed, and reset if so. @@ -1148,10 +1223,11 @@ Canvas::on_idle() d->_store_solid_background = d->solid_background; recreate_store(); d->decoupled_mode = false; - IF_LOGGING( std::cout << "Full reset" << std::endl; ) + if (d->prefs.debug.logging) std::cout << "Full reset" << std::endl; } // Todo: Consider incrementally and pre-emptively performing this operation across several frames to avoid lag spikes. + // Todo: Screw that. Just do it on the GPU. auto shift_store = [&, this] { // Recreate the store, but keep re-usable content from the old store. auto store_rect = Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() ); @@ -1174,8 +1250,7 @@ Canvas::on_idle() if (d->solid_background) { cr->set_operator(Cairo::OPERATOR_SOURCE); cr->set_source(_background); - } - else { + } else { cr->set_operator(Cairo::OPERATOR_CLEAR); } region_to_path(cr, reg); @@ -1197,7 +1272,7 @@ Canvas::on_idle() assert(d->_store_affine == _affine); // Should not be called if the affine has changed. d->_backing_store = std::move(backing_store); d->_clean_region->intersect(geom_to_cairo(d->_store_rect)); - IF_SHOW_UNCLEAN( queue_draw() ); + if (d->prefs.debug.show_unclean) queue_draw(); }; auto take_snapshot = [&, this] { @@ -1219,46 +1294,44 @@ Canvas::on_idle() take_snapshot(); // Enter decoupled mode. - IF_LOGGING( std::cout << "Entering decoupled mode" << std::endl; ) + if (d->prefs.debug.logging) std::cout << "Entering decoupled mode" << std::endl; d->decoupled_mode = true; // Note: If redrawing is fast enough to finish during the frame, then going into decoupled mode, drawing, and leaving // it again performs exactly the same rendering operations as if we had not gone into it at all. Also, no extra copies // or blits are performed, and the drawing operations done on the screen are the same. Hence this feature comes at zero cost. - } - else { + } else { // Get visible rectangle in canvas coordinates. const auto visible = Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() ); if (!d->_store_rect.intersects(visible)) { // If the store has gone completely off-screen, recreate it. recreate_store(); - IF_LOGGING( std::cout << "Recreated store" << std::endl; ) - } - else if (!d->_store_rect.contains(visible)) + if (d->prefs.debug.logging) std::cout << "Recreated store" << std::endl; + } else if (!d->_store_rect.contains(visible)) { // If the store has gone partially off-screen, shift it. shift_store(); - IF_LOGGING( std::cout << "Shifted store" << std::endl; ) + if (d->prefs.debug.logging) std::cout << "Shifted store" << std::endl; } // After these operations, the store should now be fully on-screen. assert(d->_store_rect.contains(visible)); } - } - else { + } else { // if (d->decoupled_mode) // Completely cancel the previous redraw and start again if the viewing parameters have changed too much. - auto pl = Geom::Parallelogram(get_area_world()); - pl *= d->_store_affine * _affine.inverse(); - if (!pl.intersects(d->_store_rect)) { - // Store has gone off the screen. - recreate_store(); - IF_LOGGING( std::cout << "Restarting redraw (store off-screen)" << std::endl; ) - } - else { - auto diff = calc_affine_diff(_affine, d->_store_affine); - if (diff > MAX_AFFINE_DIFF) { - // Affine has changed too much. + if (!d->prefs.debug.sticky_decoupled) { + auto pl = Geom::Parallelogram(get_area_world()); + pl *= d->_store_affine * _affine.inverse(); + if (!pl.intersects(d->_store_rect)) { + // Store has gone off the screen. recreate_store(); - IF_LOGGING( std::cout << "Restarting redraw (affine changed too much)" << std::endl; ) + if (d->prefs.debug.logging) std::cout << "Restarting redraw (store off-screen)" << std::endl; + } else { + auto diff = calc_affine_diff(_affine, d->_store_affine); + if (diff > d->prefs.max_affine_diff) { + // Affine has changed too much. + recreate_store(); + if (d->prefs.debug.logging) std::cout << "Restarting redraw (affine changed too much)" << std::endl; + } } } } @@ -1288,8 +1361,7 @@ Canvas::on_idle() if (!d->decoupled_mode) { // By a previous assertion, this always lies within the store. visible_rect = Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() ); - } - else { + } else { // Get the window rectangle transformed into canvas space. auto pl = Geom::Parallelogram(Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() )); pl *= d->_store_affine * _affine.inverse(); @@ -1313,13 +1385,12 @@ Canvas::on_idle() if (visible_rect) { paint_region = Cairo::Region::create(geom_to_cairo(*visible_rect)); paint_region->subtract(d->_clean_region); - } - else { + } else { paint_region = Cairo::Region::create(); } // Get the list of rectangles to paint, coarsened to avoid fragmentation. - auto paint_rects = coarsen(paint_region); + auto paint_rects = coarsen(paint_region, d->prefs.coarsener_min_size, d->prefs.coarsener_glue_size); // Clip the coarsened rectangles back to the visible rect, in case coarsening made them go outside it. Otherwise, we could render to outside the store! for (auto it = paint_rects.begin(); it != paint_rects.end(); ) { @@ -1327,8 +1398,7 @@ Canvas::on_idle() if (opt) { *it = *opt; ++it; - } - else { + } else { *it = paint_rects.back(); paint_rects.pop_back(); } @@ -1342,8 +1412,7 @@ Canvas::on_idle() Gdk::ModifierType mask; window->get_device_position(Gdk::Display::get_default()->get_default_seat()->get_pointer(), x, y, mask); mouse_loc = Geom::IntPoint(x, y); - } - else { + } else { mouse_loc = Geom::IntPoint(0, 0); // Doesn't particularly matter, just as long as it's initialised. } @@ -1357,7 +1426,7 @@ Canvas::on_idle() // Otherwise, the whole of the big rectangle will be rendered first, at the expense of points in other // rectangles very near to the mouse. - // Sort the rectangles to paint, sorted by distance from mouse. + // Sort the rectangles to paint by distance from mouse. std::sort(paint_rects.begin(), paint_rects.end(), [&] (const Geom::IntRect &a, const Geom::IntRect &b) { return distSq(mouse_loc, a) < distSq(mouse_loc, b); }); @@ -1368,12 +1437,10 @@ Canvas::on_idle() // Todo: Logging reveals that Inkscape often underestimates render times, and can sometimes time out quite badly, // leading to missed frames. Consider fixing the time estimator below, possibly based on run-time feedback. May // even wish to consider maintaining a coarse heatmap of the drawing to judge rendering time/order. In the meantime, - // this can be worked around be enabling the OVERBISECTION switch with a tile size of about 400. + // this can be worked around by enabling overbisection with a tile size of about 400. if (_render_mode != Inkscape::RenderMode::OUTLINE) { // Can't be too small or large gradient will be rerendered too many times! - auto prefs = Inkscape::Preferences::get(); - auto tile_multiplier = prefs->getIntLimited("/options/rendering/tile-multiplier", 16, 1, 512); - setup.max_pixels = 65536 * tile_multiplier; + setup.max_pixels = 65536 * d->prefs.tile_multiplier; } else { // Paths only. 1M is catched buffer and we need four channels. setup.max_pixels = 262144; @@ -1392,7 +1459,7 @@ Canvas::on_idle() } if (!paint_rect_internal(setup, rect)) { // Timed out. Temporarily return to GTK main loop, and come back here when next idle. - //IF_LOGGING( std::cout << "Timed out: " << g_get_monotonic_time() - setup.start_time << " us" << std::endl; ) + if (d->prefs.debug.logging) std::cout << "Timed out: " << g_get_monotonic_time() - setup.start_time << " us" << std::endl; IF_FRAMECHECK( f.subtype = 1; ) _forced_redraw_count++; return true; @@ -1403,35 +1470,36 @@ Canvas::on_idle() if (setup.disable_timeouts) { auto now = g_get_monotonic_time(); auto elapsed = now - setup.start_time; - if (elapsed > RENDER_TIME_LIMIT) { + if (elapsed > d->prefs.render_time_limit) { // Timed out. Reset counter. It will count up again on future timeouts, eventually triggering another forced redraw. _forced_redraw_count = 0; - IF_LOGGING( std::cout << "Ignored timeout: " << g_get_monotonic_time() - setup.start_time << " us" << std::endl; ) + if (d->prefs.debug.logging) std::cout << "Ignored timeout: " << g_get_monotonic_time() - setup.start_time << " us" << std::endl; IF_FRAMECHECK( f.subtype = 2; ) return false; } } - // Finished drawing. See if we need to do a final redraw at the correct affine. + // Implement the sticky decoupled mode debug switch. + if (d->decoupled_mode && d->prefs.debug.sticky_decoupled) return false; + + // Finished drawing. Handle transitions out of decoupled mode, by checking if we need to do a final redraw at the correct affine. if (d->decoupled_mode) { if (d->_store_affine == _affine) { // Content is rendered at the correct affine - exit decoupled mode and quit idle process. - IF_LOGGING( std::cout << "Finished drawing - exiting decoupled mode" << std::endl; ) + if (d->prefs.debug.logging) std::cout << "Finished drawing - exiting decoupled mode" << std::endl; // Exit decoupled mode. d->decoupled_mode = false; // Quit idle process. return false; - } - else { + } else { // Content is rendered at the wrong affine - take a new snapshot and continue idle process to continue rendering at the new affine. - IF_LOGGING( std::cout << "Scheduling final redraw" << std::endl; ) + if (d->prefs.debug.logging) std::cout << "Scheduling final redraw" << std::endl; // Snapshot and reset the backing store. take_snapshot(); // Continue idle process. return true; } - } - else { + } else { // All done, quit the idle process. IF_FRAMECHECK( f.subtype = 3; ) return false; @@ -1445,7 +1513,6 @@ Canvas::on_idle() bool Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &this_rect) { - // Find optimal rectangle dimension int bw = this_rect.width(); int bh = this_rect.height(); @@ -1454,148 +1521,142 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th return true; } - #if ENABLE_OVERBISECTION - // Aggressively subdivide into many small rectangles - if (bw > bh && bw > OVERBISECTION_SIZE) { - int mid = this_rect[Geom::X].middle(); + // Bisect rectangles that are too big. + if (!d->prefs.debug.overbisection) { + // Use original bisector. + // Todo: I still don't understand the rationale behind the following strategy. But I am keeping it as the default for now. + + /* + * 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 psycologically faster. + * + * Default is for strips mode. + */ + + if (bw * bh > setup.max_pixels) { + if (bw < bh || bh < 2 * d->prefs.tile_size) { + int mid = this_rect[Geom::X].middle(); + + auto lo = Geom::IntRect(this_rect.left(), this_rect.top(), mid, this_rect.bottom()); + auto hi = Geom::IntRect(mid, this_rect.top(), this_rect.right(), this_rect.bottom()); + + if (setup.mouse_loc[Geom::X] < mid) { + // Always paint towards the mouse first + return paint_rect_internal(setup, lo) + && paint_rect_internal(setup, hi); + } else { + return paint_rect_internal(setup, hi) + && paint_rect_internal(setup, lo); + } + } else { + int mid = this_rect[Geom::Y].middle(); - Geom::IntRect lo, hi; - lo = Geom::IntRect(this_rect.left(), this_rect.top(), mid, this_rect.bottom()); - hi = Geom::IntRect(mid, this_rect.top(), this_rect.right(), this_rect.bottom()); + auto lo = Geom::IntRect(this_rect.left(), this_rect.top(), this_rect.right(), mid ); + auto hi = Geom::IntRect(this_rect.left(), mid, this_rect.right(), this_rect.bottom()); - if (setup.mouse_loc[Geom::X] < mid) { - return paint_rect_internal(setup, lo) - && paint_rect_internal(setup, hi); - } else { - return paint_rect_internal(setup, hi) - && paint_rect_internal(setup, lo); + if (setup.mouse_loc[Geom::Y] < mid) { + // Always paint towards the mouse first + return paint_rect_internal(setup, lo) + && paint_rect_internal(setup, hi); + } else { + return paint_rect_internal(setup, hi) + && paint_rect_internal(setup, lo); + } + } } - } else if (bh > bw && bh > OVERBISECTION_SIZE) { - int mid = this_rect[Geom::Y].middle(); + } else { + // Use the new bisector, which just chops in half along the larger dimension. + if (bw > bh) { + if (bw > d->prefs.debug.overbisection_size) { + int mid = this_rect[Geom::X].middle(); - Geom::IntRect lo, hi; - lo = Geom::IntRect(this_rect.left(), this_rect.top(), this_rect.right(), mid ); - hi = Geom::IntRect(this_rect.left(), mid, this_rect.right(), this_rect.bottom()); + auto lo = Geom::IntRect(this_rect.left(), this_rect.top(), mid, this_rect.bottom()); + auto hi = Geom::IntRect(mid, this_rect.top(), this_rect.right(), this_rect.bottom()); - if (setup.mouse_loc[Geom::Y] < mid) { - return paint_rect_internal(setup, lo) - && paint_rect_internal(setup, hi); + if (setup.mouse_loc[Geom::X] < mid) { + return paint_rect_internal(setup, lo) + && paint_rect_internal(setup, hi); + } else { + return paint_rect_internal(setup, hi) + && paint_rect_internal(setup, lo); + } + } } else { - return paint_rect_internal(setup, hi) - && paint_rect_internal(setup, lo); + if (bh > d->prefs.debug.overbisection_size) { + int mid = this_rect[Geom::Y].middle(); + + auto lo = Geom::IntRect(this_rect.left(), this_rect.top(), this_rect.right(), mid ); + auto hi = Geom::IntRect(this_rect.left(), mid, this_rect.right(), this_rect.bottom()); + + if (setup.mouse_loc[Geom::Y] < mid) { + return paint_rect_internal(setup, lo) + && paint_rect_internal(setup, hi); + } else { + return paint_rect_internal(setup, hi) + && paint_rect_internal(setup, lo); + } + } } } - #endif // ENABLE_OVERBISECTION - if (bw * bh < setup.max_pixels) { - // Rectangle is small enough + // Paint the rectangle. + _drawing->setRenderMode(_render_mode); + _drawing->setColorMode(_color_mode); + paint_single_buffer(this_rect, d->_backing_store); - _drawing->setRenderMode(_render_mode); - _drawing->setColorMode(_color_mode); - paint_single_buffer(this_rect, d->_backing_store); - - /*bool outline_overlay = _drawing->outlineOverlay(); - if (_split_mode != Inkscape::SplitMode::NORMAL || outline_overlay) { - _drawing->setRenderMode(Inkscape::RenderMode::OUTLINE); - paint_single_buffer(this_rect, setup.canvas_rect, _outline_store); - if (outline_overlay) { - _drawing->setRenderMode(Inkscape::RenderMode::OUSplitTLINE_OVERLAY); - } - }*/ + // Introduce an artificial delay for each rectangle. + if (d->prefs.debug.slow_redraw) g_usleep(d->prefs.debug.slow_redraw_time); - #if ENABLE_SLOW_REDRAW - usleep(SLOW_REDRAW_TIME); // Introduce an artificial delay for each rectangle - #endif + // Mark the rectangle as clean. + d->_clean_region->do_union(geom_to_cairo(this_rect)); - Cairo::RectangleInt crect = { this_rect.left(), this_rect.top(), this_rect.width(), this_rect.height() }; - d->_clean_region->do_union( crect ); + // Mark the screen dirty. + if (!d->decoupled_mode) { + // Get rectangle needing repaint + auto repaint_rect = this_rect - Geom::IntPoint(_x0, _y0); - if (!d->decoupled_mode) { - // Get rectangle needing repaint - auto repaint_rect = this_rect - Geom::IntPoint(_x0, _y0); + // Assert that a repaint actually occurs (guaranteed because we are only asked to paint fully on-screen rectangles) + auto screen_rect = Geom::IntRect(0, 0, get_allocation().get_width(), get_allocation().get_height()); + assert(repaint_rect & screen_rect); - // Assert that a repaint actually occurs (guaranteed because we are only asked to paint fully on-screen rectangles) - auto screen_rect = Geom::IntRect(0, 0, get_allocation().get_width(), get_allocation().get_height()); - assert(repaint_rect & screen_rect); + // Schedule repaint + d->queue_draw_area(repaint_rect); + d->pending_draw = true; + } else { + // Get rectangle needing repaint (transform into screen space, take bounding box, round outwards) + auto pl = Geom::Parallelogram(this_rect); + pl *= _affine * d->_store_affine.inverse(); + pl *= Geom::Translate(-_x0, -_y0); + auto b = pl.bounds(); + auto repaint_rect = Geom::IntRect(b.min().floor(), b.max().ceil()); + // Check if repaint is necessary - some rectangles could be entirely off-screen. + auto screen_rect = Geom::IntRect(0, 0, get_allocation().get_width(), get_allocation().get_height()); + if (repaint_rect & screen_rect) { // Schedule repaint d->queue_draw_area(repaint_rect); d->pending_draw = true; } - else { - // Get rectangle needing repaint (transform into screen space, take bounding box, round outwards) - auto pl = Geom::Parallelogram(this_rect); - pl *= _affine * d->_store_affine.inverse(); - pl *= Geom::Translate(-_x0, -_y0); - auto b = pl.bounds(); - auto repaint_rect = Geom::IntRect(b.min().floor(), b.max().ceil()); - - // Check if repaint is necessary - some rectangles could be entirely off-screen - auto screen_rect = Geom::IntRect(0, 0, get_allocation().get_width(), get_allocation().get_height()); - if (repaint_rect & screen_rect) { - // Schedule repaint - d->queue_draw_area(repaint_rect); - d->pending_draw = true; - } - } - - if (!setup.disable_timeouts) { - auto now = g_get_monotonic_time(); - auto elapsed = now - setup.start_time; - if (elapsed > RENDER_TIME_LIMIT) { - return false; - } - } - - return true; } - /* - * 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 psycologically faster. - * - * Default is for strips mode. - */ - - static int TILE_SIZE = 16; - Geom::IntRect lo, hi; - - if (bw < bh || bh < 2 * TILE_SIZE) { - int mid = this_rect[Geom::X].middle(); - - lo = Geom::IntRect(this_rect.left(), this_rect.top(), mid, this_rect.bottom()); - hi = Geom::IntRect(mid, this_rect.top(), this_rect.right(), this_rect.bottom()); - - if (setup.mouse_loc[Geom::X] < mid) { - // Always paint towards the mouse first - return paint_rect_internal(setup, lo) - && paint_rect_internal(setup, hi); - } else { - return paint_rect_internal(setup, hi) - && paint_rect_internal(setup, lo); - } - } else { - int mid = this_rect[Geom::Y].middle(); - - lo = Geom::IntRect(this_rect.left(), this_rect.top(), this_rect.right(), mid ); - hi = Geom::IntRect(this_rect.left(), mid, this_rect.right(), this_rect.bottom()); - - if (setup.mouse_loc[Geom::Y] < mid) { - // Always paint towards the mouse first - return paint_rect_internal(setup, lo) - && paint_rect_internal(setup, hi); - } else { - return paint_rect_internal(setup, hi) - && paint_rect_internal(setup, lo); + // Exit if timed out. + if (!setup.disable_timeouts) { + auto now = g_get_monotonic_time(); + auto elapsed = now - setup.start_time; + if (elapsed > d->prefs.render_time_limit) { + return false; } } + + // Continue rendering. + return true; } /* @@ -1642,8 +1703,7 @@ Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtrsolid_background) { cr->set_source(_background); cr->set_operator(Cairo::OPERATOR_SOURCE); - } - else { + } else { cr->set_operator(Cairo::OPERATOR_CLEAR); } cr->paint(); @@ -1655,27 +1715,25 @@ Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtrrender(&buf); } - #if ENABLE_SHOW_REDRAW // Paint over newly drawn content with a translucent random colour - cr->set_source_rgba((rand() % 255) / 255.0, (rand() % 255) / 255.0, (rand() % 255) / 255.0, 0.2); - cr->set_operator(Cairo::OPERATOR_OVER); - cr->rectangle(0, 0, imgs->get_width(), imgs->get_height()); - cr->fill(); - #endif + if (d->prefs.debug.show_redraw) { + cr->set_source_rgba((rand() % 255) / 255.0, (rand() % 255) / 255.0, (rand() % 255) / 255.0, 0.2); + cr->set_operator(Cairo::OPERATOR_OVER); + cr->rectangle(0, 0, imgs->get_width(), imgs->get_height()); + cr->fill(); + } if (_cms_active) { - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - - auto transf = prefs->getBool("/options/displayprofile/from_display") + auto transf = d->prefs.from_display ? Inkscape::CMSSystem::getDisplayPer(_cms_key) : Inkscape::CMSSystem::getDisplayTransform(); if (transf) { imgs->flush(); - unsigned char *px = imgs->get_data(); + auto px = imgs->get_data(); int stride = imgs->get_stride(); - for (int i = 0; i < paint_rect.height(); ++i) { - unsigned char *row = px + i * stride; + for (int i = 0; i < paint_rect.height(); i++) { + auto row = px + i * stride; Inkscape::CMSSystem::doTransform(transf, row, row, paint_rect.width()); } imgs->mark_dirty(); @@ -1687,11 +1745,8 @@ Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtr& cr) { - - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - double radius = prefs->getIntLimited("/options/rendering/xray-radius", 100, 1, 1500); - +Canvas::add_clippath(const Cairo::RefPtr& cr) +{ double width = get_allocation().get_width(); double height = get_allocation().get_height(); double sx = _split_position.x(); @@ -1717,7 +1772,7 @@ Canvas::add_clippath(const Cairo::RefPtr& cr) { break; } } else { - cr->arc(sx, sy, radius, 0, 2 * M_PI); + cr->arc(sx, sy, d->prefs.x_ray_radius, 0, 2 * M_PI); } cr->clip(); -- GitLab From 5db81e1c271e4fa771aaa9d05f2dd6bf9496ffd0 Mon Sep 17 00:00:00 2001 From: PBS Date: Sun, 16 Jan 2022 19:07:20 +0900 Subject: [PATCH 15/35] Add placeholder framecheck.h. --- src/ui/widget/framecheck.h | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/ui/widget/framecheck.h diff --git a/src/ui/widget/framecheck.h b/src/ui/widget/framecheck.h new file mode 100644 index 0000000000..af704851c9 --- /dev/null +++ b/src/ui/widget/framecheck.h @@ -0,0 +1,4 @@ +#pragma once + +#define framecheck_whole_function +#define IF_FRAMECHECK(X) -- GitLab From 7e6d67acccc2e24feddfa86b03ebfdf0748ba38c Mon Sep 17 00:00:00 2001 From: PBS Date: Mon, 17 Jan 2022 03:53:06 +0900 Subject: [PATCH 16/35] Plethora of bugfixes and minor additions * Fix antialiasing somehow being enabled for transformed snapshot, causing slowdown. * Add a few more useful debug switches. * Fill out placeholder framecheck implementation. * Fix crash from idle signals being called after destruction (ugh). * Fix preferences not working. * Fix render stall due to coarsener-bisector interaction I thought would be rare. --- src/ui/CMakeLists.txt | 2 + src/ui/dialog/inkscape-preferences.cpp | 12 +- src/ui/dialog/inkscape-preferences.h | 3 + src/ui/widget/canvas.cpp | 198 ++++++++++++++----------- src/ui/widget/framecheck.cpp | 29 ++++ src/ui/widget/framecheck.h | 68 ++++++++- 6 files changed, 223 insertions(+), 89 deletions(-) create mode 100644 src/ui/widget/framecheck.cpp diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 9ad6359e1e..9466a554ed 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -188,6 +188,7 @@ set(ui_SRC widget/font-variants.cpp widget/font-variations.cpp widget/frame.cpp + widget/framecheck.cpp widget/gradient-image.cpp widget/gradient-editor.cpp widget/gradient-selector.cpp @@ -439,6 +440,7 @@ set(ui_SRC widget/font-variants.h widget/font-variations.h widget/frame.h + widget/framecheck.h widget/gradient-image.h widget/gradient-editor.h widget/gradient-selector.h diff --git a/src/ui/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp index 9135630aee..243969dff6 100644 --- a/src/ui/dialog/inkscape-preferences.cpp +++ b/src/ui/dialog/inkscape-preferences.cpp @@ -2635,6 +2635,10 @@ void InkscapePreferences::initPageRendering() _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 size + _rendering_tile_size.init("/options/rendering/tile-size", 1.0, 10000.0, 1.0, 0.0, 16.0, true, false); + _page_rendering.add_line( false, _("Tile size:"), _rendering_tile_size, "", _("The \"tile size\" parameter previously hard-coded into Inkscape's original tile bisector."), 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); @@ -2715,13 +2719,17 @@ void InkscapePreferences::initPageRendering() _page_rendering.add_line(true, "", _canvas_debug_show_unclean, "", _("Show the unclean region in red")); _canvas_debug_show_snapshot.init(_("Show snapshot"), "/options/rendering/debug/show_snapshot", false); _page_rendering.add_line(true, "", _canvas_debug_show_snapshot, "", _("Show the snapshot region in blue")); + _canvas_debug_show_clean.init(_("Show clean fragmentation"), "/options/rendering/debug/show_clean", false); + _page_rendering.add_line(true, "", _canvas_debug_show_clean, "", _("Show the outlines of the rectangles in the clean region in green")); + _canvas_debug_disable_redraw.init(_("Disable redraw"), "/options/rendering/debug/disable_redraw", false); + _page_rendering.add_line(true, "", _canvas_debug_disable_redraw, "", _("Temporarily disable the idle redraw process completely")); _canvas_debug_sticky_decoupled.init(_("Sticky decoupled mode"), "/options/rendering/debug/sticky_decoupled", false); _page_rendering.add_line(true, "", _canvas_debug_sticky_decoupled, "", _("Stay in decoupled mode even after rendering is complete")); _page_rendering.add_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); _page_rendering.add_line(true, _("Render time limit"), _canvas_render_time_limit, C_("microsecond abbreviation", "μs"), _("The maximum time allowed for a rendering time slice"), false); - _canvas_max_affine_diff.init("/options/rendering/max_affine_diff", 0.0, 100.0, 0.1, 0.0, 1.0, false, false); + _canvas_max_affine_diff.init("/options/rendering/max_affine_diff", 0.0, 100.0, 0.1, 0.0, 1.8, false, false); _page_rendering.add_line(true, _("Max affine diff"), _canvas_max_affine_diff, "", _("How much the viewing transformation can change before throwing away the current redraw and starting again"), false); _canvas_pad.init("/options/rendering/pad", 0.0, 1000.0, 1.0, 0.0, 200.0, true, false); _page_rendering.add_line(true, _("Buffer padding"), _canvas_pad, C_("pixel abbreviation", "px"), _("Use buffers bigger than the window by this amount"), false); @@ -3033,7 +3041,7 @@ void InkscapePreferences::onKBTreeEdited (const Glib::ustring& path, guint accel event.keyval = accel_key; event.state = accel_mods; event.hardware_keycode = hardware_keycode; - Gtk::AccelKey const new_shortcut_key = shortcuts.get_from_event(&event, true); + Gtk::AccelKey const new_shortcut_key = shortcuts.get_from_event(&event, true); if (!new_shortcut_key.is_null() && (new_shortcut_key.get_key() != current_shortcut_key.get_key() || diff --git a/src/ui/dialog/inkscape-preferences.h b/src/ui/dialog/inkscape-preferences.h index 41352ea521..2dfe449f79 100644 --- a/src/ui/dialog/inkscape-preferences.h +++ b/src/ui/dialog/inkscape-preferences.h @@ -323,6 +323,7 @@ protected: UI::Widget::PrefSpinButton _filter_multi_threaded; UI::Widget::PrefSpinButton _rendering_cache_size; UI::Widget::PrefSpinButton _rendering_tile_multiplier; + UI::Widget::PrefSpinButton _rendering_tile_size; UI::Widget::PrefSpinButton _rendering_xray_radius; UI::Widget::PrefSpinButton _rendering_outline_overlay_opacity; UI::Widget::PrefRadioButton _blur_quality_best; @@ -343,6 +344,8 @@ protected: UI::Widget::PrefCheckButton _canvas_debug_show_redraw; UI::Widget::PrefCheckButton _canvas_debug_show_unclean; UI::Widget::PrefCheckButton _canvas_debug_show_snapshot; + UI::Widget::PrefCheckButton _canvas_debug_show_clean; + UI::Widget::PrefCheckButton _canvas_debug_disable_redraw; UI::Widget::PrefCheckButton _canvas_debug_sticky_decoupled; UI::Widget::PrefSpinButton _canvas_debug_overbisection_size; UI::Widget::PrefSpinButton _canvas_debug_slow_redraw_time; diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index cc35449fe7..29ce2640e9 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -35,6 +35,7 @@ #include "ui/tools/tool-base.h" // Default cursor #include "framecheck.h" // For frame profiling +#define framecheck_whole_function(D) auto framecheckobj = D->prefs.debug_framecheck ? FrameCheck::Event() : FrameCheck::Event(__func__); /* * The canvas is responsible for rendering the SVG drawing with various "control" @@ -114,11 +115,12 @@ struct Pref {}; template<> struct Pref : PrefBase { - Pref(const char *path) + std::function action; + Pref(const char *path, decltype(action) action = decltype(action)()) : action(action) { auto prefs = Inkscape::Preferences::get(); t = prefs->getBool(path); - obs = prefs->createObserver(path, [this] (const Preferences::Entry &e) {t = e.getBool();}); + obs = prefs->createObserver(path, [=] (const Preferences::Entry &e) {t = e.getBool(); if (action) action();}); } }; @@ -129,7 +131,7 @@ struct Pref : PrefBase { auto prefs = Inkscape::Preferences::get(); t = prefs->getIntLimited(path, def, min, max); - obs = prefs->createObserver(path, [&, this] (const Preferences::Entry &e) {t = e.getIntLimited(def, min, max);}); + obs = prefs->createObserver(path, [=] (const Preferences::Entry &e) {t = e.getIntLimited(def, min, max);}); } }; @@ -140,57 +142,40 @@ struct Pref : PrefBase { auto prefs = Inkscape::Preferences::get(); t = prefs->getDoubleLimited(path, def, min, max); - obs = prefs->createObserver(path, [&, this] (const Preferences::Entry &e) {t = e.getDoubleLimited(def, min, max);}); + obs = prefs->createObserver(path, [=] (const Preferences::Entry &e) {t = e.getDoubleLimited(def, min, max);}); } }; -// Note: See inkscape-preferences.cpp or the Preferences GUI for documentation on the meaning of these parameters. struct Prefs { - struct Debug - { - Debug() - : framecheck("/options/rendering/debug/framecheck") - , logging("/options/rendering/debug/logging") - , overbisection("/options/rendering/debug/overbisection") - , slow_redraw("/options/rendering/debug/slow_redraw") - , show_redraw("/options/rendering/debug/show_redraw") - , show_unclean("/options/rendering/debug/show_unclean") - , show_snapshot("/options/rendering/debug/show_snapshot") - , sticky_decoupled("/options/rendering/debug/sticky_decoupled") - , overbisection_size("/options/rendering/debug/overbisection_size", 30, 1, 10000) - , slow_redraw_time("/options/rendering/debug/slow_redraw_time", 50, 0, 1000000) - {} - - // Debug switches - Pref framecheck; // Print per-frame profiling data of selected functions to a file. - Pref logging; // Log certain events to stdout. - Pref overbisection; // Bisect all tiles until they reach a minimum size. - Pref slow_redraw; // Introduce a fixed delay for each tile. - Pref show_redraw; // Paint a translucent random colour over each newly drawn tile. - Pref show_unclean; // Paint the unclean region in red. - Pref show_snapshot; // Paint the snapshot region in blue. - Pref sticky_decoupled; // Stay in decoupled mode even after rendering is completed. - - // Debug switch parameters - Pref overbisection_size; // The maxmimum allowed tile size, in pixels. - Pref slow_redraw_time; // The delay to introduce for each tile, in microseconds. - }; - - Prefs() + Prefs(Canvas *q) + // Original parameters : tile_size("/options/rendering/tile-size", 16, 1, 10000) , tile_multiplier("/options/rendering/tile-multiplier", 16, 1, 512) , x_ray_radius("/options/rendering/xray-radius", 100, 1, 1500) , from_display("/options/displayprofile/from_display") + // New parameters , render_time_limit("/options/rendering/render_time_limit", 1000, 100, 1000000) - , max_affine_diff("/options/rendering/max_affine_diff", 1.0, 0.0, 100.0) + , max_affine_diff("/options/rendering/max_affine_diff", 1.8, 0.0, 100.0) , pad("/options/rendering/pad", 200, 0, 1000) , coarsener_min_size("/options/rendering/coarsener_min_size", 200, 0, 1000) , coarsener_glue_size("/options/rendering/coarsener_glue_size", 80, 0, 1000) + // Debug switches + , debug_framecheck("/options/rendering/debug/framecheck") + , debug_logging("/options/rendering/debug/logging") + , debug_overbisection("/options/rendering/debug/overbisection") + , debug_slow_redraw("/options/rendering/debug/slow_redraw") + , debug_show_redraw("/options/rendering/debug/show_redraw") + , debug_show_unclean("/options/rendering/debug/show_unclean", [=] {q->queue_draw();}) + , debug_show_snapshot("/options/rendering/debug/show_snapshot", [=] {q->queue_draw();}) + , debug_show_clean("/options/rendering/debug/show_clean", [=] {q->queue_draw();}) + , debug_disable_redraw("/options/rendering/debug/disable_redraw", [=] {q->request_update();}) + , debug_sticky_decoupled("/options/rendering/debug/sticky_decoupled", [=] {q->request_update();}) + // Debug switch options + , debug_overbisection_size("/options/rendering/debug/overbisection_size", 30, 1, 10000) + , debug_slow_redraw_time("/options/rendering/debug/slow_redraw_time", 50, 0, 1000000) {} - Debug debug; - // Original parameters Pref tile_size; Pref tile_multiplier; @@ -203,6 +188,22 @@ struct Prefs Pref pad; // Use buffers bigger than the window by this amount so that scrolling becomes a no-op most of the time. Pref coarsener_min_size; // Parameters given to the coarsener algorithm when used to coarsen the paint region. You probably don't want to change these! Pref coarsener_glue_size; // (the same) + + // Debug switches + Pref debug_framecheck; // Print per-frame profiling data of selected functions to a file. + Pref debug_logging; // Log certain events to stdout. + Pref debug_overbisection; // Bisect all tiles until they reach a minimum size. + Pref debug_slow_redraw; // Introduce a fixed delay for each tile. + Pref debug_show_redraw; // Paint a translucent random colour over each newly drawn tile. + Pref debug_show_unclean; // Paint the unclean region in red. + Pref debug_show_snapshot; // Paint the snapshot region in blue. + Pref debug_show_clean; // Paint the outlines of the subrectangles of the clean region in green. + Pref debug_disable_redraw; // Temporarily disable the idle redraw process completely. + Pref debug_sticky_decoupled; // Stay in decoupled mode even after rendering is completed. + + // Debug switch options + Pref debug_overbisection_size; // The maxmimum allowed tile size, in pixels. + Pref debug_slow_redraw_time; // The delay to introduce for each tile, in microseconds. }; class CanvasPrivate @@ -211,7 +212,7 @@ public: friend class Canvas; Canvas *q; - CanvasPrivate(Canvas *q) : q(q) {} + CanvasPrivate(Canvas *q) : q(q), prefs(q) {} // Important global properties of all the stores. If these change, all the stores must be recreated. int _device_scale = 1; @@ -265,7 +266,7 @@ void CanvasPrivate::schedule_bucket_emptier() void CanvasPrivate::empty_bucket() { - IF_FRAMECHECK(framecheck_whole_function) + framecheck_whole_function(this) auto bucket2 = std::move(bucket); @@ -341,7 +342,7 @@ Canvas::Canvas() _canvas_item_root->set_name("CanvasItemGroup:Root"); _canvas_item_root->set_canvas(this); - if (d->prefs.debug.show_redraw) srand(g_get_monotonic_time()); // Initialise seed for random colours. + if (d->prefs.debug_show_redraw) srand(g_get_monotonic_time()); // Initialise seed for random colours. } Canvas::~Canvas() @@ -351,6 +352,10 @@ Canvas::~Canvas() _drawing = nullptr; _in_destruction = true; + // Disconnect signals. Otherwise called after destructor and crashes. + d->hipri_idle.disconnect(); + d->lopri_idle.disconnect(); + // Remove entire CanvasItem tree. delete _canvas_item_root; } @@ -414,7 +419,7 @@ Canvas::redraw_all() } d->_clean_region = Cairo::Region::create(); // Empty region (i.e. everything is dirty). add_idle(); - if (d->prefs.debug.show_unclean) queue_draw(); + if (d->prefs.debug_show_unclean) queue_draw(); } /** @@ -447,7 +452,7 @@ Canvas::redraw_area(int x0, int y0, int x1, int y1) Cairo::RectangleInt crect = { x0, y0, x1-x0, y1-y0 }; d->_clean_region->subtract(crect); add_idle(); - if (d->prefs.debug.show_unclean) queue_draw(); + if (d->prefs.debug_show_unclean) queue_draw(); } void @@ -943,7 +948,8 @@ region_to_path(const Cairo::RefPtr &cr, const Cairo::RefPtr &cr) { - IF_FRAMECHECK( Prof f; ) + auto f = FrameCheck::Event(); + if (d->prefs.debug_framecheck) f = FrameCheck::Event("on_draw"); // sp_canvas_item_recursive_print_tree(0, _root); // canvas_item_print_tree(_canvas_item_root); @@ -960,7 +966,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) // Blit background if not solid. (If solid, it is baked into the stores.) if (!d->solid_background) { - IF_FRAMECHECK( f = Prof("background"); ) + if (d->prefs.debug_framecheck) f = FrameCheck::Event("background"); cr->save(); cr->set_operator(Cairo::OPERATOR_SOURCE); cr->set_source(_background); @@ -970,7 +976,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) if (!d->decoupled_mode) { // Blit backing store to screen. - IF_FRAMECHECK( f = Prof("draw"); ) + if (d->prefs.debug_framecheck) f = FrameCheck::Event("draw"); cr->save(); cr->set_operator(d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); cr->set_source(d->_backing_store, d->_store_rect.left() - _x0, d->_store_rect.top() - _y0); @@ -982,7 +988,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) // Blit background to complement of both clean regions, if solid (and therefore not already drawn). if (d->solid_background) { - IF_FRAMECHECK( f = Prof("composite", 2); ) + if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 2); cr->save(); cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD); cr->rectangle(0, 0, get_allocation().get_width(), get_allocation().get_height()); @@ -1000,7 +1006,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) } // Draw transformed snapshot, clipped to its clean region and the complement of the backing store's clean region. - IF_FRAMECHECK( f = Prof("composite", 1); ) + if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 1); cr->save(); cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD); cr->rectangle(0, 0, get_allocation().get_width(), get_allocation().get_height()); @@ -1013,9 +1019,9 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->clip(); cr->set_source(d->_snapshot_store, d->_snapshot_rect.left(), d->_snapshot_rect.top()); cr->set_operator(d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); - cr->paint(); Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST); - if (d->prefs.debug.show_snapshot) { + cr->paint(); + if (d->prefs.debug_show_snapshot) { cr->set_source_rgba(0, 0, 1, 0.2); cr->set_operator(Cairo::OPERATOR_OVER); cr->paint(); @@ -1023,7 +1029,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->restore(); // Draw transformed store, clipped to clean region. - IF_FRAMECHECK( f = Prof("composite", 0); ) + if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 0); cr->save(); cr->translate(-_x0, -_y0); cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); @@ -1037,8 +1043,8 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) } // Paint unclean regions in red. - if (d->prefs.debug.show_unclean) { - IF_FRAMECHECK( f = Prof("paint_unclean"); ) + if (d->prefs.debug_show_unclean) { + if (d->prefs.debug_framecheck) f = FrameCheck::Event("paint_unclean"); auto reg = Cairo::Region::create(geom_to_cairo(d->_store_rect)); reg->subtract(d->_clean_region); cr->save(); @@ -1052,6 +1058,20 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->restore(); } + // Paint internal edges of clean region in green. + if (d->prefs.debug_show_clean) { + if (d->prefs.debug_framecheck) f = FrameCheck::Event("paint_clean"); + cr->save(); + cr->translate(-_x0, -_y0); + if (d->decoupled_mode) { + cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); + } + cr->set_source_rgba(0, 0.7, 0, 0.4); + region_to_path(cr, d->_clean_region); + cr->stroke(); + cr->restore(); + } + // Process bucketed events as soon as possible after draw. if (d->pending_draw) { if (!d->bucket.empty()) { @@ -1074,7 +1094,7 @@ Canvas::update_canvas_item_ctrl_sizes(int size_index) void Canvas::add_idle() { - IF_FRAMECHECK(framecheck_whole_function) + framecheck_whole_function(d) if (_in_destruction) { std::cerr << "Canvas::add_idle: Called after canvas destroyed!" << std::endl; @@ -1107,8 +1127,6 @@ calc_affine_diff(const Geom::Affine &a, const Geom::Affine &b) { auto coarsen(const Cairo::RefPtr ®ion, int min_size, int glue_size) { - IF_FRAMECHECK(framecheck_whole_function); - // Sort the rects by minExtent struct Compare { @@ -1179,7 +1197,7 @@ coarsen(const Cairo::RefPtr ®ion, int min_size, int glue_size) bool Canvas::on_idle() { - IF_FRAMECHECK( auto f = FuncProf(); ) + framecheck_whole_function(d) assert(_canvas_item_root); @@ -1214,7 +1232,7 @@ Canvas::on_idle() } cr->paint(); d->_clean_region = Cairo::Region::create(); - if (d->prefs.debug.show_unclean) queue_draw(); + if (d->prefs.debug_show_unclean) queue_draw(); }; // Determine the rendering parameters have changed, and reset if so. @@ -1223,7 +1241,7 @@ Canvas::on_idle() d->_store_solid_background = d->solid_background; recreate_store(); d->decoupled_mode = false; - if (d->prefs.debug.logging) std::cout << "Full reset" << std::endl; + if (d->prefs.debug_logging) std::cout << "Full reset" << std::endl; } // Todo: Consider incrementally and pre-emptively performing this operation across several frames to avoid lag spikes. @@ -1272,7 +1290,7 @@ Canvas::on_idle() assert(d->_store_affine == _affine); // Should not be called if the affine has changed. d->_backing_store = std::move(backing_store); d->_clean_region->intersect(geom_to_cairo(d->_store_rect)); - if (d->prefs.debug.show_unclean) queue_draw(); + if (d->prefs.debug_show_unclean) queue_draw(); }; auto take_snapshot = [&, this] { @@ -1294,7 +1312,7 @@ Canvas::on_idle() take_snapshot(); // Enter decoupled mode. - if (d->prefs.debug.logging) std::cout << "Entering decoupled mode" << std::endl; + if (d->prefs.debug_logging) std::cout << "Entering decoupled mode" << std::endl; d->decoupled_mode = true; // Note: If redrawing is fast enough to finish during the frame, then going into decoupled mode, drawing, and leaving @@ -1306,31 +1324,31 @@ Canvas::on_idle() if (!d->_store_rect.intersects(visible)) { // If the store has gone completely off-screen, recreate it. recreate_store(); - if (d->prefs.debug.logging) std::cout << "Recreated store" << std::endl; + if (d->prefs.debug_logging) std::cout << "Recreated store" << std::endl; } else if (!d->_store_rect.contains(visible)) { // If the store has gone partially off-screen, shift it. shift_store(); - if (d->prefs.debug.logging) std::cout << "Shifted store" << std::endl; + if (d->prefs.debug_logging) std::cout << "Shifted store" << std::endl; } // After these operations, the store should now be fully on-screen. assert(d->_store_rect.contains(visible)); } } else { // if (d->decoupled_mode) // Completely cancel the previous redraw and start again if the viewing parameters have changed too much. - if (!d->prefs.debug.sticky_decoupled) { + if (!d->prefs.debug_sticky_decoupled) { auto pl = Geom::Parallelogram(get_area_world()); pl *= d->_store_affine * _affine.inverse(); if (!pl.intersects(d->_store_rect)) { // Store has gone off the screen. recreate_store(); - if (d->prefs.debug.logging) std::cout << "Restarting redraw (store off-screen)" << std::endl; + if (d->prefs.debug_logging) std::cout << "Restarting redraw (store off-screen)" << std::endl; } else { auto diff = calc_affine_diff(_affine, d->_store_affine); if (diff > d->prefs.max_affine_diff) { // Affine has changed too much. recreate_store(); - if (d->prefs.debug.logging) std::cout << "Restarting redraw (affine changed too much)" << std::endl; + if (d->prefs.debug_logging) std::cout << "Restarting redraw (affine changed too much)" << std::endl; } } } @@ -1392,7 +1410,7 @@ Canvas::on_idle() // Get the list of rectangles to paint, coarsened to avoid fragmentation. auto paint_rects = coarsen(paint_region, d->prefs.coarsener_min_size, d->prefs.coarsener_glue_size); - // Clip the coarsened rectangles back to the visible rect, in case coarsening made them go outside it. Otherwise, we could render to outside the store! + // Clip the coarsened rectangles back to the visible rect, in case coarsening made them go outside it. for (auto it = paint_rects.begin(); it != paint_rects.end(); ) { auto opt = *it & *visible_rect; if (opt) { @@ -1451,6 +1469,11 @@ Canvas::on_idle() // Need to go through all places where forced redraw is requested, and check whether it is helpful or harmful. setup.disable_timeouts = false; + // If asked to, don't paint anything and instead halt the idle process. + if (d->prefs.debug_disable_redraw) { + return false; + } + // Paint the rectangles. for (const auto &rect : paint_rects) { if (rect.hasZeroArea()) { @@ -1459,8 +1482,8 @@ Canvas::on_idle() } if (!paint_rect_internal(setup, rect)) { // Timed out. Temporarily return to GTK main loop, and come back here when next idle. - if (d->prefs.debug.logging) std::cout << "Timed out: " << g_get_monotonic_time() - setup.start_time << " us" << std::endl; - IF_FRAMECHECK( f.subtype = 1; ) + if (d->prefs.debug_logging) std::cout << "Timed out: " << g_get_monotonic_time() - setup.start_time << " us" << std::endl; + framecheckobj.subtype = 1; _forced_redraw_count++; return true; } @@ -1473,27 +1496,27 @@ Canvas::on_idle() if (elapsed > d->prefs.render_time_limit) { // Timed out. Reset counter. It will count up again on future timeouts, eventually triggering another forced redraw. _forced_redraw_count = 0; - if (d->prefs.debug.logging) std::cout << "Ignored timeout: " << g_get_monotonic_time() - setup.start_time << " us" << std::endl; - IF_FRAMECHECK( f.subtype = 2; ) + if (d->prefs.debug_logging) std::cout << "Ignored timeout: " << g_get_monotonic_time() - setup.start_time << " us" << std::endl; + framecheckobj.subtype = 2; return false; } } // Implement the sticky decoupled mode debug switch. - if (d->decoupled_mode && d->prefs.debug.sticky_decoupled) return false; + if (d->decoupled_mode && d->prefs.debug_sticky_decoupled) return false; // Finished drawing. Handle transitions out of decoupled mode, by checking if we need to do a final redraw at the correct affine. if (d->decoupled_mode) { if (d->_store_affine == _affine) { // Content is rendered at the correct affine - exit decoupled mode and quit idle process. - if (d->prefs.debug.logging) std::cout << "Finished drawing - exiting decoupled mode" << std::endl; + if (d->prefs.debug_logging) std::cout << "Finished drawing - exiting decoupled mode" << std::endl; // Exit decoupled mode. d->decoupled_mode = false; // Quit idle process. return false; } else { // Content is rendered at the wrong affine - take a new snapshot and continue idle process to continue rendering at the new affine. - if (d->prefs.debug.logging) std::cout << "Scheduling final redraw" << std::endl; + if (d->prefs.debug_logging) std::cout << "Scheduling final redraw" << std::endl; // Snapshot and reset the backing store. take_snapshot(); // Continue idle process. @@ -1501,7 +1524,7 @@ Canvas::on_idle() } } else { // All done, quit the idle process. - IF_FRAMECHECK( f.subtype = 3; ) + framecheckobj.subtype = 3; return false; } } @@ -1521,10 +1544,17 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th return true; } + // Due to coarsening, it's possible for bisected rectangles to lie entirely inside the clean region. These can be discarded, and in fact must be, + // else we risk rendering only clean rectangles for the whole frame, which would lead to a render stall. + if (d->_clean_region->contains_rectangle(geom_to_cairo(this_rect)) == Cairo::REGION_OVERLAP_IN) { + // Rectangle is already clean. + return true; + } + // Bisect rectangles that are too big. - if (!d->prefs.debug.overbisection) { + if (!d->prefs.debug_overbisection) { // Use original bisector. - // Todo: I still don't understand the rationale behind the following strategy. But I am keeping it as the default for now. + // Todo: I still don't understand the rationale behind the following strategy. But I'm keeping it default for now. /* * Determine redraw strategy: @@ -1535,7 +1565,7 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th * 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 psycologically faster. + * Seems to be somewhat psychologically faster. * * Default is for strips mode. */ @@ -1574,7 +1604,7 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th } else { // Use the new bisector, which just chops in half along the larger dimension. if (bw > bh) { - if (bw > d->prefs.debug.overbisection_size) { + if (bw > d->prefs.debug_overbisection_size) { int mid = this_rect[Geom::X].middle(); auto lo = Geom::IntRect(this_rect.left(), this_rect.top(), mid, this_rect.bottom()); @@ -1589,7 +1619,7 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th } } } else { - if (bh > d->prefs.debug.overbisection_size) { + if (bh > d->prefs.debug_overbisection_size) { int mid = this_rect[Geom::Y].middle(); auto lo = Geom::IntRect(this_rect.left(), this_rect.top(), this_rect.right(), mid ); @@ -1612,7 +1642,7 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th paint_single_buffer(this_rect, d->_backing_store); // Introduce an artificial delay for each rectangle. - if (d->prefs.debug.slow_redraw) g_usleep(d->prefs.debug.slow_redraw_time); + if (d->prefs.debug_slow_redraw) g_usleep(d->prefs.debug_slow_redraw_time); // Mark the rectangle as clean. d->_clean_region->do_union(geom_to_cairo(this_rect)); @@ -1716,7 +1746,7 @@ Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtrprefs.debug.show_redraw) { + if (d->prefs.debug_show_redraw) { cr->set_source_rgba((rand() % 255) / 255.0, (rand() % 255) / 255.0, (rand() % 255) / 255.0, 0.2); cr->set_operator(Cairo::OPERATOR_OVER); cr->rectangle(0, 0, imgs->get_width(), imgs->get_height()); @@ -1988,7 +2018,7 @@ Canvas::pick_current_item(GdkEvent *event) bool Canvas::emit_event(GdkEvent *event) { - IF_FRAMECHECK(framecheck_whole_function) + framecheck_whole_function(d) if (event == d->ignore) { return false; diff --git a/src/ui/widget/framecheck.cpp b/src/ui/widget/framecheck.cpp new file mode 100644 index 0000000000..532a921f0b --- /dev/null +++ b/src/ui/widget/framecheck.cpp @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include +#include +#include +#include "framecheck.h" +namespace fs = std::filesystem; + +namespace Inkscape { +namespace FrameCheck { + +std::ostream &logfile() +{ + 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; +} + +} // namespace FrameCheck +} // namespace Inkscape diff --git a/src/ui/widget/framecheck.h b/src/ui/widget/framecheck.h index af704851c9..36eeea16ef 100644 --- a/src/ui/widget/framecheck.h +++ b/src/ui/widget/framecheck.h @@ -1,4 +1,66 @@ -#pragma once +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Functions for logging timing events. + * Copyright (C) 2022 PBS + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ -#define framecheck_whole_function -#define IF_FRAMECHECK(X) +#ifndef FRAMECHECK_H +#define FRAMECHECK_H + +#include +#include + +namespace Inkscape { +namespace FrameCheck { + +extern std::ostream &logfile(); + +// RAII object that logs a timing event for the duration of its lifetime. +struct Event +{ + gint64 start; + const char *name; + int subtype; + + Event() : start(-1) {} + + Event(const char *name, int subtype = 0) : start(g_get_monotonic_time()), name(name), subtype(subtype) {} + + Event(Event &&p) + { + movefrom(p); + } + + ~Event() + { + finish(); + } + + Event &operator=(Event &&p) + { + finish(); + movefrom(p); + return *this; + } + + void movefrom(Event &p) + { + start = p.start; + name = p.name; + subtype = p.subtype; + p.start = -1; + } + + void finish() + { + if (start != -1) { + logfile() << name << ' ' << start << ' ' << g_get_monotonic_time() << ' ' << subtype << '\n'; + } + } +}; + +} // namespace FrameCheck +} // namespace Inkscape + +#endif // FRAMECHECK_H -- GitLab From 77e56baea01358b0cb37627ae2079de1fb2de4fd Mon Sep 17 00:00:00 2001 From: PBS Date: Mon, 17 Jan 2022 18:26:33 +0900 Subject: [PATCH 17/35] Fix HiDPI, MacOS build, and debug switch backwardsness. * UnFUBAR HiDPI on all platforms. * Replace std with boost filesystem due to broken C++17 on old MacOS. * Fix framecheck debug switch being on when it was off and off when it was on, generating enormous, boundlessly files. --- src/ui/widget/canvas.cpp | 6 ++++-- src/ui/widget/framecheck.cpp | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 29ce2640e9..3b7d7ac3e6 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -35,7 +35,7 @@ #include "ui/tools/tool-base.h" // Default cursor #include "framecheck.h" // For frame profiling -#define framecheck_whole_function(D) auto framecheckobj = D->prefs.debug_framecheck ? FrameCheck::Event() : FrameCheck::Event(__func__); +#define framecheck_whole_function(D) auto framecheckobj = D->prefs.debug_framecheck ? FrameCheck::Event(__func__) : FrameCheck::Event(); /* * The canvas is responsible for rendering the SVG drawing with various "control" @@ -1155,7 +1155,7 @@ coarsen(const Cairo::RefPtr ®ion, int min_size, int glue_size) auto glue_zone = rect; glue_zone.expandBy(glue_size); - // Absorb rectangles in the glue zone. We could make this a lot faster, but it's already fast enough. + // Absorb rectangles in the glue zone. We could do better algorithmically speaking, but in real life it's already plenty fast. auto orig = rect; for (auto it = rects.begin(); it != rects.end(); ) { if (glue_zone.contains(*it)) { @@ -1222,6 +1222,7 @@ Canvas::on_idle() if (!d->_backing_store || d->_backing_store->get_width() != desired_width || d->_backing_store->get_height() != desired_height) { // Todo: Stop cairo unnecessarily pre-filling the store with transparency below if d->solid_background is true. d->_backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, desired_width, desired_height); + cairo_surface_set_device_scale(d->_backing_store->cobj(), d->_device_scale, d->_device_scale); // No C++ API! } auto cr = Cairo::Context::create(d->_backing_store); if (d->solid_background) { @@ -1252,6 +1253,7 @@ Canvas::on_idle() store_rect.expandBy(pad); // Todo: Stop cairo unnecessarily pre-filling the store with transparency below if d->solid_background is true. auto backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, store_rect.width() * d->_device_scale, store_rect.height() * d->_device_scale); + cairo_surface_set_device_scale(backing_store->cobj(), d->_device_scale, d->_device_scale); // No C++ API! // Determine the geometry of the shift. auto shift = store_rect.min() - d->_store_rect.min(); diff --git a/src/ui/widget/framecheck.cpp b/src/ui/widget/framecheck.cpp index 532a921f0b..27b3d5bf36 100644 --- a/src/ui/widget/framecheck.cpp +++ b/src/ui/widget/framecheck.cpp @@ -1,9 +1,9 @@ // 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 = std::filesystem; +namespace fs = boost::filesystem; namespace Inkscape { namespace FrameCheck { @@ -15,7 +15,7 @@ std::ostream &logfile() 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; + 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; -- GitLab From cc7de64c1df5a2c867e7300f76b5f8d4fc73e02d Mon Sep 17 00:00:00 2001 From: PBS Date: Wed, 19 Jan 2022 06:17:07 +0900 Subject: [PATCH 18/35] Important fixes to idle system, bucketer, initialisation and resize Fixes * The glitchy black frames at startup * Late-frame redraw interrupts observed in framecheck on MacOS Also * Side-effect: Replace earlier MacOS crash fix with the real solution * Clean up preferences system * Move more to CanvasPrivate --- src/ui/widget/canvas.cpp | 478 +++++++++++++++++++++------------------ src/ui/widget/canvas.h | 69 ++---- 2 files changed, 271 insertions(+), 276 deletions(-) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 3b7d7ac3e6..229934d634 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -116,7 +116,7 @@ template<> struct Pref : PrefBase { std::function action; - Pref(const char *path, decltype(action) action = decltype(action)()) : action(action) + Pref(const char *path) { auto prefs = Inkscape::Preferences::get(); t = prefs->getBool(path); @@ -127,11 +127,12 @@ struct Pref : PrefBase template<> struct Pref : PrefBase { + std::function action; Pref(const char *path, int def, int min, int max) { auto prefs = Inkscape::Preferences::get(); t = prefs->getIntLimited(path, def, min, max); - obs = prefs->createObserver(path, [=] (const Preferences::Entry &e) {t = e.getIntLimited(def, min, max);}); + obs = prefs->createObserver(path, [=] (const Preferences::Entry &e) {t = e.getIntLimited(def, min, max); if (action) action(t);}); } }; @@ -148,62 +149,33 @@ struct Pref : PrefBase struct Prefs { - Prefs(Canvas *q) - // Original parameters - : tile_size("/options/rendering/tile-size", 16, 1, 10000) - , tile_multiplier("/options/rendering/tile-multiplier", 16, 1, 512) - , x_ray_radius("/options/rendering/xray-radius", 100, 1, 1500) - , from_display("/options/displayprofile/from_display") - // New parameters - , render_time_limit("/options/rendering/render_time_limit", 1000, 100, 1000000) - , max_affine_diff("/options/rendering/max_affine_diff", 1.8, 0.0, 100.0) - , pad("/options/rendering/pad", 200, 0, 1000) - , coarsener_min_size("/options/rendering/coarsener_min_size", 200, 0, 1000) - , coarsener_glue_size("/options/rendering/coarsener_glue_size", 80, 0, 1000) - // Debug switches - , debug_framecheck("/options/rendering/debug/framecheck") - , debug_logging("/options/rendering/debug/logging") - , debug_overbisection("/options/rendering/debug/overbisection") - , debug_slow_redraw("/options/rendering/debug/slow_redraw") - , debug_show_redraw("/options/rendering/debug/show_redraw") - , debug_show_unclean("/options/rendering/debug/show_unclean", [=] {q->queue_draw();}) - , debug_show_snapshot("/options/rendering/debug/show_snapshot", [=] {q->queue_draw();}) - , debug_show_clean("/options/rendering/debug/show_clean", [=] {q->queue_draw();}) - , debug_disable_redraw("/options/rendering/debug/disable_redraw", [=] {q->request_update();}) - , debug_sticky_decoupled("/options/rendering/debug/sticky_decoupled", [=] {q->request_update();}) - // Debug switch options - , debug_overbisection_size("/options/rendering/debug/overbisection_size", 30, 1, 10000) - , debug_slow_redraw_time("/options/rendering/debug/slow_redraw_time", 50, 0, 1000000) - {} - // Original parameters - Pref tile_size; - Pref tile_multiplier; - Pref x_ray_radius; - Pref from_display; + Pref tile_size = Pref ("/options/rendering/tile-size", 16, 1, 10000); + Pref tile_multiplier = Pref ("/options/rendering/tile-multiplier", 16, 1, 512); + Pref x_ray_radius = Pref ("/options/rendering/xray-radius", 100, 1, 1500); + Pref from_display = Pref ("/options/displayprofile/from_display"); + Pref grabsize = Pref ("/options/grabsize/value", 3, 1, 15); // New parameters - Pref render_time_limit; // The maximum time allowed for a rendering time slice (on_idle) before we return to the main loop, in microseconds. - Pref max_affine_diff; // How much the viewing transformation can change before we decide to throw away the current redraw and start again. - Pref pad; // Use buffers bigger than the window by this amount so that scrolling becomes a no-op most of the time. - Pref coarsener_min_size; // Parameters given to the coarsener algorithm when used to coarsen the paint region. You probably don't want to change these! - Pref coarsener_glue_size; // (the same) + Pref render_time_limit = Pref ("/options/rendering/render_time_limit", 1000, 100, 1000000); + Pref max_affine_diff = Pref("/options/rendering/max_affine_diff", 1.8, 0.0, 100.0); + Pref pad = Pref ("/options/rendering/pad", 200, 0, 1000); + Pref coarsener_min_size = Pref ("/options/rendering/coarsener_min_size", 200, 0, 1000); + Pref coarsener_glue_size = Pref ("/options/rendering/coarsener_glue_size", 80, 0, 1000); // Debug switches - Pref debug_framecheck; // Print per-frame profiling data of selected functions to a file. - Pref debug_logging; // Log certain events to stdout. - Pref debug_overbisection; // Bisect all tiles until they reach a minimum size. - Pref debug_slow_redraw; // Introduce a fixed delay for each tile. - Pref debug_show_redraw; // Paint a translucent random colour over each newly drawn tile. - Pref debug_show_unclean; // Paint the unclean region in red. - Pref debug_show_snapshot; // Paint the snapshot region in blue. - Pref debug_show_clean; // Paint the outlines of the subrectangles of the clean region in green. - Pref debug_disable_redraw; // Temporarily disable the idle redraw process completely. - Pref debug_sticky_decoupled; // Stay in decoupled mode even after rendering is completed. - - // Debug switch options - Pref debug_overbisection_size; // The maxmimum allowed tile size, in pixels. - Pref debug_slow_redraw_time; // The delay to introduce for each tile, in microseconds. + Pref debug_framecheck = Pref ("/options/rendering/debug/framecheck"); + Pref debug_logging = Pref ("/options/rendering/debug/logging"); + Pref debug_overbisection = Pref ("/options/rendering/debug/overbisection"); + Pref debug_overbisection_size = Pref ("/options/rendering/debug/overbisection_size", 30, 1, 10000); + Pref debug_slow_redraw = Pref ("/options/rendering/debug/slow_redraw"); + Pref debug_slow_redraw_time = Pref ("/options/rendering/debug/slow_redraw_time", 50, 0, 1000000); + Pref debug_show_redraw = Pref ("/options/rendering/debug/show_redraw"); + Pref debug_show_unclean = Pref ("/options/rendering/debug/show_unclean"); + Pref debug_show_snapshot = Pref ("/options/rendering/debug/show_snapshot"); + Pref debug_show_clean = Pref ("/options/rendering/debug/show_clean"); + Pref debug_disable_redraw = Pref ("/options/rendering/debug/disable_redraw"); + Pref debug_sticky_decoupled = Pref ("/options/rendering/debug/sticky_decoupled"); }; class CanvasPrivate @@ -212,7 +184,12 @@ public: friend class Canvas; Canvas *q; - CanvasPrivate(Canvas *q) : q(q), prefs(q) {} + CanvasPrivate(Canvas *q) : q(q) {} + + void add_idle(); + bool on_idle(); + bool paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &this_rect); + void paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtr &store); // Important global properties of all the stores. If these change, all the stores must be recreated. int _device_scale = 1; @@ -239,7 +216,8 @@ public: std::vector> bucket; sigc::connection bucket_emptier; - bool pending_draw = false; + bool pending_bucket_emptier = false; + bool idle_running = false; void schedule_bucket_emptier(); void empty_bucket(); @@ -251,6 +229,11 @@ public: void queue_draw_area(Geom::IntRect &rect); Prefs prefs; + + bool on_hipri_idle(); + bool on_lopri_idle(); + + void update_ctrl_sizes(int size) {q->_canvas_item_root->update_canvas_item_ctrl_sizes(size);} }; void CanvasPrivate::schedule_bucket_emptier() @@ -268,6 +251,8 @@ void CanvasPrivate::empty_bucket() { framecheck_whole_function(this) + pending_bucket_emptier = false; + auto bucket2 = std::move(bucket); for (auto &event : bucket2) { @@ -311,7 +296,7 @@ void CanvasPrivate::queue_draw_area(Geom::IntRect &rect) } Canvas::Canvas() - : _size_observer(this, "/options/grabsize/value"), d(std::make_unique(this)) + : d(std::make_unique(this)) { set_name("InkscapeCanvas"); @@ -327,6 +312,13 @@ Canvas::Canvas() Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK ); + // Preferences + d->prefs.grabsize.action = [=] (int size) {_canvas_item_root->update_canvas_item_ctrl_sizes(size);}; + d->prefs.debug_show_unclean.action = [=] {queue_draw();}; + d->prefs.debug_show_clean.action = [=] {queue_draw();}; + d->prefs.debug_disable_redraw.action = [=] {queue_draw();}; + d->prefs.debug_sticky_decoupled.action = [=] {d->add_idle();}; + // Give _pick_event an initial definition. _pick_event.type = GDK_LEAVE_NOTIFY; _pick_event.crossing.x = 0; @@ -402,7 +394,7 @@ Canvas::set_affine(Geom::Affine const &affine) _affine = affine; - add_idle(); + d->add_idle(); queue_draw(); } @@ -418,7 +410,7 @@ Canvas::redraw_all() return; } d->_clean_region = Cairo::Region::create(); // Empty region (i.e. everything is dirty). - add_idle(); + d->add_idle(); if (d->prefs.debug_show_unclean) queue_draw(); } @@ -451,7 +443,7 @@ Canvas::redraw_area(int x0, int y0, int x1, int y1) Cairo::RectangleInt crect = { x0, y0, x1-x0, y1-y0 }; d->_clean_region->subtract(crect); - add_idle(); + d->add_idle(); if (d->prefs.debug_show_unclean) queue_draw(); } @@ -488,7 +480,7 @@ Canvas::request_update() _need_update = true; // Trigger the idle process to perform the update. - add_idle(); + d->add_idle(); } /** @@ -508,7 +500,7 @@ Canvas::scroll_to(Geom::Point const &c) _x0 = x; _y0 = y; - add_idle(); + d->add_idle(); queue_draw(); if (auto grid = dynamic_cast(get_parent())) { @@ -896,6 +888,22 @@ Canvas::on_motion_notify_event(GdkEventMotion *motion_event) return status; } +/** + * Resize handler + */ +void Canvas::on_size_allocate(Gtk::Allocation &allocation) +{ + parent_type::on_size_allocate(allocation); + assert(allocation == get_allocation()); + d->add_idle(); // Trigger the size update to be applied to the stores before the next call to on_draw. +} + +void Canvas::on_realize() +{ + parent_type::on_realize(); + d->add_idle(); +} + auto geom_to_cairo(Geom::IntRect rect) { @@ -949,21 +957,21 @@ bool Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) { auto f = FrameCheck::Event(); - if (d->prefs.debug_framecheck) f = FrameCheck::Event("on_draw"); // sp_canvas_item_recursive_print_tree(0, _root); // canvas_item_print_tree(_canvas_item_root); - // Todo: On MacOS, GTK event loop ordering problems result in this function being called before the first call to hipri_idle, which is technically - // impossible because hipri_idle has higher priority than draw. When this is fixed, the following can be made back into an assertion. - if (!d->_backing_store) { - std::cout << "CRASH AVERTED: on_draw called before first call to on_idle!" << std::endl; - // Should really paint the background, but not worth it. - return true; - } - assert(_drawing); + // 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(); + } + // Blit background if not solid. (If solid, it is baked into the stores.) if (!d->solid_background) { if (d->prefs.debug_framecheck) f = FrameCheck::Event("background"); @@ -1073,11 +1081,12 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) } // Process bucketed events as soon as possible after draw. - if (d->pending_draw) { + if (d->pending_bucket_emptier) { if (!d->bucket.empty()) { d->schedule_bucket_emptier(); + } else { + d->pending_bucket_emptier = false; } - d->pending_draw = false; } // Todo: Add back X-ray view. @@ -1086,28 +1095,29 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) } void -Canvas::update_canvas_item_ctrl_sizes(int size_index) -{ - _canvas_item_root->update_canvas_item_ctrl_sizes(size_index); -} - -void -Canvas::add_idle() +CanvasPrivate::add_idle() { - framecheck_whole_function(d) + framecheck_whole_function(this) - if (_in_destruction) { + if (q->_in_destruction) { std::cerr << "Canvas::add_idle: Called after canvas destroyed!" << std::endl; return; } - if (!d->hipri_idle.connected()) { - d->hipri_idle = Glib::signal_idle().connect([this] {on_idle(); return false;}, G_PRIORITY_HIGH_IDLE + 15); // after resize, before draw + if (!q->get_realized()) { + // We can safely discard events until realized, because we will run add_idle in the on_realize handler later in initialisation. + return; } - if (!d->lopri_idle.connected()) { - d->lopri_idle = Glib::signal_idle().connect(sigc::mem_fun(*this, &Canvas::on_idle), G_PRIORITY_DEFAULT_IDLE); + 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; } auto @@ -1195,81 +1205,99 @@ coarsen(const Cairo::RefPtr ®ion, int min_size, int glue_size) } bool -Canvas::on_idle() +CanvasPrivate::on_hipri_idle() { - framecheck_whole_function(d) + if (idle_running) { + idle_running = on_idle(); + } + return false; +} - assert(_canvas_item_root); +bool +CanvasPrivate::on_lopri_idle() +{ + if (idle_running) { + idle_running = on_idle(); + } + return idle_running; +} - if (_in_destruction) { +bool +CanvasPrivate::on_idle() +{ + framecheck_whole_function(this) + + assert(q->_canvas_item_root); + + if (q->_in_destruction) { std::cerr << "Canvas::on_idle: Called after canvas destroyed!" << std::endl; return false; } // Quit idle process if not supposed to be drawing. - if (!_drawing || _drawing_disabled) { + if (!q->_drawing || q->_drawing_disabled) { return false; } - const Geom::IntPoint pad(d->prefs.pad, d->prefs.pad); + const Geom::IntPoint pad(prefs.pad, prefs.pad); auto recreate_store = [&, this] { // Recreate the store at the current affine so that it covers the visible region. - d->_store_rect = Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() ); - d->_store_rect.expandBy(pad); - d->_store_affine = _affine; - int desired_width = d->_store_rect.width() * d->_device_scale; - int desired_height = d->_store_rect.height() * d->_device_scale; - if (!d->_backing_store || d->_backing_store->get_width() != desired_width || d->_backing_store->get_height() != desired_height) { - // Todo: Stop cairo unnecessarily pre-filling the store with transparency below if d->solid_background is true. - d->_backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, desired_width, desired_height); - cairo_surface_set_device_scale(d->_backing_store->cobj(), d->_device_scale, d->_device_scale); // No C++ API! + _store_rect = Geom::IntRect::from_xywh( q->_x0, q->_y0, q->get_allocation().get_width(), q->get_allocation().get_height() ); + _store_rect.expandBy(pad); + _store_affine = q->_affine; + int desired_width = _store_rect.width() * _device_scale; + int desired_height = _store_rect.height() * _device_scale; + if (!_backing_store || _backing_store->get_width() != desired_width || _backing_store->get_height() != desired_height) { + // Todo: Stop cairo unnecessarily pre-filling the store with transparency below if solid_background is true. + _backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, desired_width, desired_height); + cairo_surface_set_device_scale(_backing_store->cobj(), _device_scale, _device_scale); // No C++ API! } - auto cr = Cairo::Context::create(d->_backing_store); - if (d->solid_background) { + auto cr = Cairo::Context::create(_backing_store); + if (solid_background) { cr->set_operator(Cairo::OPERATOR_SOURCE); - cr->set_source(_background); + cr->set_source(q->_background); } else { cr->set_operator(Cairo::OPERATOR_CLEAR); } cr->paint(); - d->_clean_region = Cairo::Region::create(); - if (d->prefs.debug_show_unclean) queue_draw(); + _clean_region = Cairo::Region::create(); + if (prefs.debug_show_unclean) q->queue_draw(); }; // Determine the rendering parameters have changed, and reset if so. - if (!d->_backing_store || d->_device_scale != get_scale_factor() || d->_store_solid_background != d->solid_background) { - d->_device_scale = get_scale_factor(); - d->_store_solid_background = d->solid_background; + if (!_backing_store || _device_scale != q->get_scale_factor() || _store_solid_background != solid_background) { + _device_scale = q->get_scale_factor(); + _store_solid_background = solid_background; recreate_store(); - d->decoupled_mode = false; - if (d->prefs.debug_logging) std::cout << "Full reset" << std::endl; + decoupled_mode = false; + if (prefs.debug_logging) std::cout << "Full reset" << std::endl; } // Todo: Consider incrementally and pre-emptively performing this operation across several frames to avoid lag spikes. // Todo: Screw that. Just do it on the GPU. auto shift_store = [&, this] { // Recreate the store, but keep re-usable content from the old store. - auto store_rect = Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() ); + auto store_rect = Geom::IntRect::from_xywh( q->_x0, q->_y0, q->get_allocation().get_width(), q->get_allocation().get_height() ); store_rect.expandBy(pad); - // Todo: Stop cairo unnecessarily pre-filling the store with transparency below if d->solid_background is true. - auto backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, store_rect.width() * d->_device_scale, store_rect.height() * d->_device_scale); - cairo_surface_set_device_scale(backing_store->cobj(), d->_device_scale, d->_device_scale); // No C++ API! + // Todo: Stop cairo unnecessarily pre-filling the store with transparency below if solid_background is true. + auto backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, store_rect.width() * _device_scale, store_rect.height() * _device_scale); + cairo_surface_set_device_scale(backing_store->cobj(), _device_scale, _device_scale); // No C++ API! // Determine the geometry of the shift. - auto shift = store_rect.min() - d->_store_rect.min(); - auto reuse_rect = store_rect & d->_store_rect; + auto shift = store_rect.min() - _store_rect.min(); + auto reuse_rect = store_rect & _store_rect; assert(reuse_rect); // Should not be called if there is no overlap. auto cr = Cairo::Context::create(backing_store); // Paint background into region not covered by next operation. - if (d->solid_background) { + if (solid_background) { auto reg = Cairo::Region::create(geom_to_cairo(store_rect)); reg->subtract(geom_to_cairo(*reuse_rect)); reg->translate(-store_rect.left(), -store_rect.top()); cr->save(); - if (d->solid_background) { + if (solid_background) { cr->set_operator(Cairo::OPERATOR_SOURCE); - cr->set_source(_background); + cr->set_source(q->_background); } else { cr->set_operator(Cairo::OPERATOR_CLEAR); } @@ -1282,75 +1310,75 @@ Canvas::on_idle() cr->save(); cr->rectangle(reuse_rect->left() - store_rect.left(), reuse_rect->top() - store_rect.top(), reuse_rect->width(), reuse_rect->height()); cr->clip(); - cr->set_source(d->_backing_store, -shift.x(), -shift.y()); + cr->set_source(_backing_store, -shift.x(), -shift.y()); cr->set_operator(Cairo::OPERATOR_SOURCE); cr->paint(); cr->restore(); // Set the result as the new backing store. - d->_store_rect = store_rect; - assert(d->_store_affine == _affine); // Should not be called if the affine has changed. - d->_backing_store = std::move(backing_store); - d->_clean_region->intersect(geom_to_cairo(d->_store_rect)); - if (d->prefs.debug_show_unclean) queue_draw(); + _store_rect = store_rect; + assert(_store_affine == q->_affine); // Should not be called if the affine has changed. + _backing_store = std::move(backing_store); + _clean_region->intersect(geom_to_cairo(_store_rect)); + if (prefs.debug_show_unclean) q->queue_draw(); }; auto take_snapshot = [&, this] { // Copy the backing store to the snapshot, leaving us temporarily in an invalid state. - std::swap(d->_snapshot_store, d->_backing_store); // This will re-use the old snapshot store later if possible. - d->_snapshot_rect = d->_store_rect; - d->_snapshot_affine = d->_store_affine; - d->_snapshot_clean_region = d->_clean_region->copy(); + std::swap(_snapshot_store, _backing_store); // This will re-use the old snapshot store later if possible. + _snapshot_rect = _store_rect; + _snapshot_affine = _store_affine; + _snapshot_clean_region = _clean_region->copy(); // Recreate the backing store, making the state valid again. recreate_store(); }; // Handle transitions and actions in response to viewport changes. - if (!d->decoupled_mode) { + if (!decoupled_mode) { // Enter decoupled mode if the affine has changed from what the backing store was drawn at. - if (_affine != d->_store_affine) { + if (q->_affine != _store_affine) { // Snapshot and reset the backing store. take_snapshot(); // Enter decoupled mode. - if (d->prefs.debug_logging) std::cout << "Entering decoupled mode" << std::endl; - d->decoupled_mode = true; + if (prefs.debug_logging) std::cout << "Entering decoupled mode" << std::endl; + decoupled_mode = true; // Note: If redrawing is fast enough to finish during the frame, then going into decoupled mode, drawing, and leaving // it again performs exactly the same rendering operations as if we had not gone into it at all. Also, no extra copies // or blits are performed, and the drawing operations done on the screen are the same. Hence this feature comes at zero cost. } else { // Get visible rectangle in canvas coordinates. - const auto visible = Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() ); - if (!d->_store_rect.intersects(visible)) { + const auto visible = Geom::IntRect::from_xywh( q->_x0, q->_y0, q->get_allocation().get_width(), q->get_allocation().get_height() ); + if (!_store_rect.intersects(visible)) { // If the store has gone completely off-screen, recreate it. recreate_store(); - if (d->prefs.debug_logging) std::cout << "Recreated store" << std::endl; - } else if (!d->_store_rect.contains(visible)) + if (prefs.debug_logging) std::cout << "Recreated store" << std::endl; + } else if (!_store_rect.contains(visible)) { // If the store has gone partially off-screen, shift it. shift_store(); - if (d->prefs.debug_logging) std::cout << "Shifted store" << std::endl; + if (prefs.debug_logging) std::cout << "Shifted store" << std::endl; } // After these operations, the store should now be fully on-screen. - assert(d->_store_rect.contains(visible)); + assert(_store_rect.contains(visible)); } - } else { // if (d->decoupled_mode) + } else { // if (decoupled_mode) // Completely cancel the previous redraw and start again if the viewing parameters have changed too much. - if (!d->prefs.debug_sticky_decoupled) { - auto pl = Geom::Parallelogram(get_area_world()); - pl *= d->_store_affine * _affine.inverse(); - if (!pl.intersects(d->_store_rect)) { + if (!prefs.debug_sticky_decoupled) { + auto pl = Geom::Parallelogram(q->get_area_world()); + pl *= _store_affine * q->_affine.inverse(); + if (!pl.intersects(_store_rect)) { // Store has gone off the screen. recreate_store(); - if (d->prefs.debug_logging) std::cout << "Restarting redraw (store off-screen)" << std::endl; + if (prefs.debug_logging) std::cout << "Restarting redraw (store off-screen)" << std::endl; } else { - auto diff = calc_affine_diff(_affine, d->_store_affine); - if (diff > d->prefs.max_affine_diff) { + auto diff = calc_affine_diff(q->_affine, _store_affine); + if (diff > prefs.max_affine_diff) { // Affine has changed too much. recreate_store(); - if (d->prefs.debug_logging) std::cout << "Restarting redraw (affine changed too much)" << std::endl; + if (prefs.debug_logging) std::cout << "Restarting redraw (affine changed too much)" << std::endl; } } } @@ -1358,17 +1386,17 @@ Canvas::on_idle() // Assert that _clean_region is a subregion of _store_rect. #ifndef NDEBUG - auto tmp = d->_clean_region->copy(); - tmp->subtract(geom_to_cairo(d->_store_rect)); + auto tmp = _clean_region->copy(); + tmp->subtract(geom_to_cairo(_store_rect)); assert(tmp->empty()); #endif // Ensure the geometry is up-to-date and in the right place. - auto affine = d->decoupled_mode ? d->_store_affine : _affine; - if (_need_update || d->geom_affine != affine) { - _canvas_item_root->update(affine); - d->geom_affine = affine; - _need_update = false; + auto affine = decoupled_mode ? _store_affine : q->_affine; + if (q->_need_update || geom_affine != affine) { + q->_canvas_item_root->update(affine); + geom_affine = affine; + q->_need_update = false; } // Todo: if redrawing is measured to be extremely heavy, we could consider deferring the start of redrawing until the viewing parameters have changed sufficiently much. @@ -1378,41 +1406,42 @@ Canvas::on_idle() // Get the rectangle of store that is visible. Geom::OptIntRect visible_rect; - if (!d->decoupled_mode) { + if (!decoupled_mode) { // By a previous assertion, this always lies within the store. - visible_rect = Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() ); + visible_rect = Geom::IntRect::from_xywh( q->_x0, q->_y0, q->get_allocation().get_width(), q->get_allocation().get_height() ); } else { // Get the window rectangle transformed into canvas space. - auto pl = Geom::Parallelogram(Geom::IntRect::from_xywh( _x0, _y0, get_allocation().get_width(), get_allocation().get_height() )); - pl *= d->_store_affine * _affine.inverse(); + auto pl = Geom::Parallelogram(Geom::IntRect::from_xywh( q->_x0, q->_y0, q->get_allocation().get_width(), q->get_allocation().get_height() )); + pl *= _store_affine * q->_affine.inverse(); // Get its bounding box, rounded outwards. auto b = pl.bounds(); auto bi = Geom::IntRect(b.min().floor(), b.max().ceil()); // The visible rect is the intersection of this with the store - visible_rect = bi & d->_store_rect; + visible_rect = bi & _store_rect; // Note: We could have used a smaller region containing pl consisting of many rectangles, rather than a single bounding box. // However, while this uses less pixels it also uses more rectangles, so is NOT necessarily an optimisation and could backfire. // For this reason, and for simplicity, we use the bounding rect. (Note: Slightly invalidated by addition of coarsener below.) } // The visible rectangle must be a subrectangle of store. - assert(d->_store_rect.contains(visible_rect)); + assert(_store_rect.contains(visible_rect)); // Get the region to paint, which is the visible rectangle minus the clean region (both subregions of store). Cairo::RefPtr paint_region; if (visible_rect) { paint_region = Cairo::Region::create(geom_to_cairo(*visible_rect)); - paint_region->subtract(d->_clean_region); + paint_region->subtract(_clean_region); } else { paint_region = Cairo::Region::create(); } // Get the list of rectangles to paint, coarsened to avoid fragmentation. - auto paint_rects = coarsen(paint_region, d->prefs.coarsener_min_size, d->prefs.coarsener_glue_size); + auto paint_rects = coarsen(paint_region, prefs.coarsener_min_size, prefs.coarsener_glue_size); // Clip the coarsened rectangles back to the visible rect, in case coarsening made them go outside it. + // Todo: That should be impossible, but somehow it occasionally happens. Understand! for (auto it = paint_rects.begin(); it != paint_rects.end(); ) { auto opt = *it & *visible_rect; if (opt) { @@ -1426,7 +1455,7 @@ Canvas::on_idle() // Get the mouse position in screen space. Geom::IntPoint mouse_loc; - if (auto window = get_window()) { + if (auto window = q->get_window()) { int x; int y; Gdk::ModifierType mask; @@ -1437,9 +1466,9 @@ Canvas::on_idle() } // Map the mouse to canvas space. - mouse_loc += Geom::IntPoint(_x0, _y0); - if (d->decoupled_mode) { - mouse_loc = geom_act(d->_store_affine * _affine.inverse(), mouse_loc); + mouse_loc += Geom::IntPoint(q->_x0, q->_y0); + if (decoupled_mode) { + mouse_loc = geom_act(_store_affine * q->_affine.inverse(), mouse_loc); } // Todo: Consider further subdividing the region if the mouse lies on the edge of a big rectangle. @@ -1458,9 +1487,9 @@ Canvas::on_idle() // leading to missed frames. Consider fixing the time estimator below, possibly based on run-time feedback. May // even wish to consider maintaining a coarse heatmap of the drawing to judge rendering time/order. In the meantime, // this can be worked around by enabling overbisection with a tile size of about 400. - if (_render_mode != Inkscape::RenderMode::OUTLINE) { + if (q->_render_mode != Inkscape::RenderMode::OUTLINE) { // Can't be too small or large gradient will be rerendered too many times! - setup.max_pixels = 65536 * d->prefs.tile_multiplier; + setup.max_pixels = 65536 * prefs.tile_multiplier; } else { // Paths only. 1M is catched buffer and we need four channels. setup.max_pixels = 262144; @@ -1472,21 +1501,21 @@ Canvas::on_idle() setup.disable_timeouts = false; // If asked to, don't paint anything and instead halt the idle process. - if (d->prefs.debug_disable_redraw) { + if (prefs.debug_disable_redraw) { return false; } // Paint the rectangles. for (const auto &rect : paint_rects) { if (rect.hasZeroArea()) { - // Todo: I'm not sure if these can get through or not. If not, remove this check. This ia a question about cairo. + // Todo: I'm not sure if these can get through or not. If not, remove this check. This is a question about cairo. continue; } if (!paint_rect_internal(setup, rect)) { // Timed out. Temporarily return to GTK main loop, and come back here when next idle. - if (d->prefs.debug_logging) std::cout << "Timed out: " << g_get_monotonic_time() - setup.start_time << " us" << std::endl; + if (prefs.debug_logging) std::cout << "Timed out: " << g_get_monotonic_time() - setup.start_time << " us" << std::endl; framecheckobj.subtype = 1; - _forced_redraw_count++; + q->_forced_redraw_count++; return true; } } @@ -1495,30 +1524,30 @@ Canvas::on_idle() if (setup.disable_timeouts) { auto now = g_get_monotonic_time(); auto elapsed = now - setup.start_time; - if (elapsed > d->prefs.render_time_limit) { + if (elapsed > prefs.render_time_limit) { // Timed out. Reset counter. It will count up again on future timeouts, eventually triggering another forced redraw. - _forced_redraw_count = 0; - if (d->prefs.debug_logging) std::cout << "Ignored timeout: " << g_get_monotonic_time() - setup.start_time << " us" << std::endl; + q->_forced_redraw_count = 0; + if (prefs.debug_logging) std::cout << "Ignored timeout: " << g_get_monotonic_time() - setup.start_time << " us" << std::endl; framecheckobj.subtype = 2; return false; } } // Implement the sticky decoupled mode debug switch. - if (d->decoupled_mode && d->prefs.debug_sticky_decoupled) return false; + if (decoupled_mode && prefs.debug_sticky_decoupled) return false; // Finished drawing. Handle transitions out of decoupled mode, by checking if we need to do a final redraw at the correct affine. - if (d->decoupled_mode) { - if (d->_store_affine == _affine) { + if (decoupled_mode) { + if (_store_affine == q->_affine) { // Content is rendered at the correct affine - exit decoupled mode and quit idle process. - if (d->prefs.debug_logging) std::cout << "Finished drawing - exiting decoupled mode" << std::endl; + if (prefs.debug_logging) std::cout << "Finished drawing - exiting decoupled mode" << std::endl; // Exit decoupled mode. - d->decoupled_mode = false; + decoupled_mode = false; // Quit idle process. return false; } else { // Content is rendered at the wrong affine - take a new snapshot and continue idle process to continue rendering at the new affine. - if (d->prefs.debug_logging) std::cout << "Scheduling final redraw" << std::endl; + if (prefs.debug_logging) std::cout << "Scheduling final redraw" << std::endl; // Snapshot and reset the backing store. take_snapshot(); // Continue idle process. @@ -1536,7 +1565,7 @@ Canvas::on_idle() * Queues Gtk redraw of widget. */ bool -Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &this_rect) +CanvasPrivate::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &this_rect) { int bw = this_rect.width(); int bh = this_rect.height(); @@ -1547,14 +1576,14 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th } // Due to coarsening, it's possible for bisected rectangles to lie entirely inside the clean region. These can be discarded, and in fact must be, - // else we risk rendering only clean rectangles for the whole frame, which would lead to a render stall. - if (d->_clean_region->contains_rectangle(geom_to_cairo(this_rect)) == Cairo::REGION_OVERLAP_IN) { + // because otherwise we risk rendering only clean rectangles for the whole frame, which would lead to a render stall. + if (_clean_region->contains_rectangle(geom_to_cairo(this_rect)) == Cairo::REGION_OVERLAP_IN) { // Rectangle is already clean. return true; } // Bisect rectangles that are too big. - if (!d->prefs.debug_overbisection) { + if (!prefs.debug_overbisection) { // Use original bisector. // Todo: I still don't understand the rationale behind the following strategy. But I'm keeping it default for now. @@ -1573,7 +1602,7 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th */ if (bw * bh > setup.max_pixels) { - if (bw < bh || bh < 2 * d->prefs.tile_size) { + if (bw < bh || bh < 2 * prefs.tile_size) { int mid = this_rect[Geom::X].middle(); auto lo = Geom::IntRect(this_rect.left(), this_rect.top(), mid, this_rect.bottom()); @@ -1606,7 +1635,7 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th } else { // Use the new bisector, which just chops in half along the larger dimension. if (bw > bh) { - if (bw > d->prefs.debug_overbisection_size) { + if (bw > prefs.debug_overbisection_size) { int mid = this_rect[Geom::X].middle(); auto lo = Geom::IntRect(this_rect.left(), this_rect.top(), mid, this_rect.bottom()); @@ -1621,7 +1650,7 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th } } } else { - if (bh > d->prefs.debug_overbisection_size) { + if (bh > prefs.debug_overbisection_size) { int mid = this_rect[Geom::Y].middle(); auto lo = Geom::IntRect(this_rect.left(), this_rect.top(), this_rect.right(), mid ); @@ -1639,42 +1668,42 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th } // Paint the rectangle. - _drawing->setRenderMode(_render_mode); - _drawing->setColorMode(_color_mode); - paint_single_buffer(this_rect, d->_backing_store); + q->_drawing->setRenderMode(q->_render_mode); + q->_drawing->setColorMode(q->_color_mode); + paint_single_buffer(this_rect, _backing_store); // Introduce an artificial delay for each rectangle. - if (d->prefs.debug_slow_redraw) g_usleep(d->prefs.debug_slow_redraw_time); + if (prefs.debug_slow_redraw) g_usleep(prefs.debug_slow_redraw_time); // Mark the rectangle as clean. - d->_clean_region->do_union(geom_to_cairo(this_rect)); + _clean_region->do_union(geom_to_cairo(this_rect)); // Mark the screen dirty. - if (!d->decoupled_mode) { + if (!decoupled_mode) { // Get rectangle needing repaint - auto repaint_rect = this_rect - Geom::IntPoint(_x0, _y0); + auto repaint_rect = this_rect - Geom::IntPoint(q->_x0, q->_y0); // Assert that a repaint actually occurs (guaranteed because we are only asked to paint fully on-screen rectangles) - auto screen_rect = Geom::IntRect(0, 0, get_allocation().get_width(), get_allocation().get_height()); + auto screen_rect = Geom::IntRect(0, 0, q->get_allocation().get_width(), q->get_allocation().get_height()); assert(repaint_rect & screen_rect); // Schedule repaint - d->queue_draw_area(repaint_rect); - d->pending_draw = true; + queue_draw_area(repaint_rect); // Guarantees on_draw will be called in the future. + pending_bucket_emptier = true; // On the next call to on_draw, schedules the bucket emptier. } else { // Get rectangle needing repaint (transform into screen space, take bounding box, round outwards) auto pl = Geom::Parallelogram(this_rect); - pl *= _affine * d->_store_affine.inverse(); - pl *= Geom::Translate(-_x0, -_y0); + pl *= q->_affine * _store_affine.inverse(); + pl *= Geom::Translate(-q->_x0, -q->_y0); auto b = pl.bounds(); auto repaint_rect = Geom::IntRect(b.min().floor(), b.max().ceil()); // Check if repaint is necessary - some rectangles could be entirely off-screen. - auto screen_rect = Geom::IntRect(0, 0, get_allocation().get_width(), get_allocation().get_height()); + auto screen_rect = Geom::IntRect(0, 0, q->get_allocation().get_width(), q->get_allocation().get_height()); if (repaint_rect & screen_rect) { // Schedule repaint - d->queue_draw_area(repaint_rect); - d->pending_draw = true; + queue_draw_area(repaint_rect); + pending_bucket_emptier = true; } } @@ -1682,7 +1711,7 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th if (!setup.disable_timeouts) { auto now = g_get_monotonic_time(); auto elapsed = now - setup.start_time; - if (elapsed > d->prefs.render_time_limit) { + if (elapsed > prefs.render_time_limit) { return false; } } @@ -1698,12 +1727,12 @@ Canvas::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &th * store: Cairo surface to draw on. */ void -Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtr &store) +CanvasPrivate::paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtr &store) { // Make sure the following code does not go outside of store's data assert(store); assert(store->get_format() == Cairo::FORMAT_ARGB32); - assert(d->_store_rect.contains(paint_rect)); + assert(_store_rect.contains(paint_rect)); // Create temporary surface that draws directly to store. store->flush(); @@ -1714,26 +1743,26 @@ Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtrcobj(), &x_scale, &y_scale); // No C++ API! - assert (d->_device_scale == (int) x_scale); - assert (d->_device_scale == (int) y_scale); + assert (_device_scale == (int) x_scale); + assert (_device_scale == (int) y_scale); // Move to the correct row. - data += stride * (paint_rect.top() - d->_store_rect.top()) * (int)y_scale; + data += stride * (paint_rect.top() - _store_rect.top()) * (int)y_scale; // Move to the correct column. - data += 4 * (paint_rect.left() - d->_store_rect.left()) * (int)x_scale; + data += 4 * (paint_rect.left() - _store_rect.left()) * (int)x_scale; auto imgs = Cairo::ImageSurface::create(data, Cairo::FORMAT_ARGB32, - paint_rect.width() * d->_device_scale, - paint_rect.height() * d->_device_scale, + paint_rect.width() * _device_scale, + paint_rect.height() * _device_scale, stride); - cairo_surface_set_device_scale(imgs->cobj(), d->_device_scale, d->_device_scale); // No C++ API! + cairo_surface_set_device_scale(imgs->cobj(), _device_scale, _device_scale); // No C++ API! auto cr = Cairo::Context::create(imgs); // Clear background cr->save(); - if (d->solid_background) { - cr->set_source(_background); + if (solid_background) { + cr->set_source(q->_background); cr->set_operator(Cairo::OPERATOR_SOURCE); } else { cr->set_operator(Cairo::OPERATOR_CLEAR); @@ -1742,22 +1771,22 @@ Canvas::paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtrrestore(); // Render drawing on top of background. - if (_canvas_item_root->is_visible()) { - auto buf = Inkscape::CanvasItemBuffer{ paint_rect, d->_device_scale, cr }; - _canvas_item_root->render(&buf); + if (q->_canvas_item_root->is_visible()) { + auto buf = Inkscape::CanvasItemBuffer{ paint_rect, _device_scale, cr }; + q->_canvas_item_root->render(&buf); } // Paint over newly drawn content with a translucent random colour - if (d->prefs.debug_show_redraw) { + if (prefs.debug_show_redraw) { cr->set_source_rgba((rand() % 255) / 255.0, (rand() % 255) / 255.0, (rand() % 255) / 255.0, 0.2); cr->set_operator(Cairo::OPERATOR_OVER); cr->rectangle(0, 0, imgs->get_width(), imgs->get_height()); cr->fill(); } - if (_cms_active) { - auto transf = d->prefs.from_display - ? Inkscape::CMSSystem::getDisplayPer(_cms_key) + if (q->_cms_active) { + auto transf = prefs.from_display + ? Inkscape::CMSSystem::getDisplayPer(q->_cms_key) : Inkscape::CMSSystem::getDisplayTransform(); if (transf) { @@ -2087,9 +2116,10 @@ Canvas::emit_event(GdkEvent *event) } d->bucket.emplace_back(event_copy); - if (!d->pending_draw) { + if (!d->pending_bucket_emptier) { add_tick_callback([this] (const Glib::RefPtr&) { d->schedule_bucket_emptier(); + d->pending_bucket_emptier = true; return false; }); } diff --git a/src/ui/widget/canvas.h b/src/ui/widget/canvas.h index 791685cbb1..3ae2859093 100644 --- a/src/ui/widget/canvas.h +++ b/src/ui/widget/canvas.h @@ -122,42 +122,31 @@ public: protected: - void get_preferred_width_vfunc( int& minimum_width, int& natural_width ) const override; - void get_preferred_height_vfunc(int& minimum_height, int& natural_height) const override; + void get_preferred_width_vfunc (int &minimum_width, int &natural_width ) const override; + void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override; // Event handlers - bool on_scroll_event( GdkEventScroll *scroll_event) override; - bool on_button_event( GdkEventButton *button_event); - bool on_button_press_event( GdkEventButton *button_event) override; - bool on_button_release_event(GdkEventButton *button_event) override; - bool on_enter_notify_event( GdkEventCrossing *crossing_event) override; - bool on_leave_notify_event( GdkEventCrossing *crossing_event) override; - bool on_focus_in_event( GdkEventFocus *focus_event ) override; - bool on_key_press_event( GdkEventKey *key_event ) override; - bool on_key_release_event( GdkEventKey *key_event ) override; - bool on_motion_notify_event( GdkEventMotion *motion_event) override; - - // Painting - bool on_draw(const Cairo::RefPtr& cr) override; - - void update_canvas_item_ctrl_sizes(int size_index); + bool on_scroll_event (GdkEventScroll* ) override; + bool on_button_event (GdkEventButton* ); + bool on_button_press_event (GdkEventButton* ) override; + bool on_button_release_event(GdkEventButton* ) override; + bool on_enter_notify_event (GdkEventCrossing*) override; + bool on_leave_notify_event (GdkEventCrossing*) override; + bool on_focus_in_event (GdkEventFocus* ) override; + bool on_key_press_event (GdkEventKey* ) override; + bool on_key_release_event (GdkEventKey* ) override; + bool on_motion_notify_event (GdkEventMotion* ) override; + + void on_realize() override; + void on_size_allocate(Gtk::Allocation&) override; + bool on_draw(const Cairo::RefPtr&) override; private: - // ======== Functions ======= - void add_idle(); - bool on_idle(); - // Drawing (internal overloads) 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); - // Painting - - // In order they are called in painting. - bool paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &this_rect); - void paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtr &store); - void add_clippath(const Cairo::RefPtr& cr); void set_cursor(); @@ -220,31 +209,7 @@ private: bool _in_destruction = false; // ======= CAIRO ======= ... Keep in one place - Cairo::RefPtr _background; ///< The background of the widget. - - // Used to update CanvasItemCtrl's when size changed. - class CanvasPrefObserver : public Inkscape::Preferences::Observer { - public: - CanvasPrefObserver(Inkscape::UI::Widget::Canvas *canvas, Glib::ustring const &path) - : Inkscape::Preferences::Observer(path) - , _canvas(canvas) - { - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); - prefs->addObserver(*this); - } - ~CanvasPrefObserver() override = default; - private: - void notify(Inkscape::Preferences::Entry const &entry) override - { - if (entry.getEntryName() == "value") { - int size = entry.getIntLimited(3, 1, 15); - _canvas->update_canvas_item_ctrl_sizes(size); - } - } - Inkscape::UI::Widget::Canvas *_canvas = nullptr; - }; - - CanvasPrefObserver _size_observer; + Cairo::RefPtr _background; ///< The background of the widget. // Opaque pointer to implementation friend class CanvasPrivate; -- GitLab From 6779eccf7040342d48460f085fa4c2b2b8865cb2 Mon Sep 17 00:00:00 2001 From: PBS Date: Wed, 19 Jan 2022 07:17:55 +0900 Subject: [PATCH 19/35] Revert 57e91d740f7e617e3fdacb771069e0faf, closing 16-year old FIXME. This from 2006: This is a really really bad bug. GTK is severely broken and does all kinds of weird things when you run main loop iterations in the middle of a screen redraw caused by an event handler. Not only does it die when trying to blink (!) but it also sends multiple value-changed events, loses release events, etc. It's crazy. I fixed some of this weirdness by disabling interruptibility, but this is no solution, of course, just a crude workaround. For example it still crashes (with the same "can't blink without focus" brain damage) when entering a value to a selector toolbar spinbutton, if the selected object is big and blurred (and thus slow to redraw). So it looks like I need to disable interruptibility for ALL spinbuttons in our UI which would make it kinda meaningless. I'm fairly sure that this bug was fixed in GTK a long time ago. If not, then it would have blown up new Canvas by now. And besides, render-on- idle is now such an indispensible part of Canvas that if, somehow, this bug were still around, we would now have to fix it properly. --- src/ui/toolbar/select-toolbar.cpp | 6 ------ src/ui/widget/canvas.h | 2 -- src/ui/widget/fill-style.cpp | 12 ------------ src/ui/widget/selected-style.cpp | 12 ------------ 4 files changed, 32 deletions(-) diff --git a/src/ui/toolbar/select-toolbar.cpp b/src/ui/toolbar/select-toolbar.cpp index f1c03190cf..68e9782047 100644 --- a/src/ui/toolbar/select-toolbar.cpp +++ b/src/ui/toolbar/select-toolbar.cpp @@ -353,9 +353,6 @@ SelectToolbar::any_value_changed(Glib::RefPtr& adj) if (actionkey != nullptr) { - // FIXME: fix for GTK breakage, see comment in SelectedStyle::on_opacity_changed - desktop->getCanvas()->forced_redraws_start(0); - Inkscape::Preferences *prefs = Inkscape::Preferences::get(); bool transform_stroke = prefs->getBool("/options/transform/stroke", true); bool preserve = prefs->getBool("/options/preservetransform/value", false); @@ -373,9 +370,6 @@ SelectToolbar::any_value_changed(Glib::RefPtr& adj) selection->applyAffine(scaler); DocumentUndo::maybeDone(document, actionkey, _("Transform by toolbar"), INKSCAPE_ICON("tool-pointer")); - - // resume interruptibility - desktop->getCanvas()->forced_redraws_stop(); } _update = false; diff --git a/src/ui/widget/canvas.h b/src/ui/widget/canvas.h index 3ae2859093..a7a74aa623 100644 --- a/src/ui/widget/canvas.h +++ b/src/ui/widget/canvas.h @@ -97,7 +97,6 @@ public: Cairo::RefPtr get_backing_store() const; // Background rotation preview Cairo::RefPtr get_background_pattern() const { return _background; } - // For a GTK bug (see SelectedStyle::on_opacity_changed()). void forced_redraws_start(int count, bool reset = true); void forced_redraws_stop() { _forced_redraw_limit = -1; } @@ -199,7 +198,6 @@ private: std::string _cms_key; bool _cms_active = false; - // For a GTK bug (see SelectedStyle::on_opacity_changed()). int _forced_redraw_limit = -1; int _forced_redraw_count = 0; diff --git a/src/ui/widget/fill-style.cpp b/src/ui/widget/fill-style.cpp index fd68bcb298..b5b9d9abfa 100644 --- a/src/ui/widget/fill-style.cpp +++ b/src/ui/widget/fill-style.cpp @@ -40,7 +40,6 @@ #include "style.h" #include "ui/icon-names.h" -#include "ui/widget/canvas.h" // Forced redraws // These can be deleted once we sort out the libart dependence. @@ -411,23 +410,12 @@ void FillNStroke::updateFromPaint(bool switch_style) } case UI::Widget::PaintSelector::MODE_SOLID_COLOR: { - if (kind == FILL) { - // FIXME: fix for GTK breakage, see comment in SelectedStyle::on_opacity_changed; here it results in - // losing release events - _desktop->getCanvas()->forced_redraws_start(0); - } - _psel->setFlatColor(_desktop, (kind == FILL) ? "fill" : "stroke", (kind == FILL) ? "fill-opacity" : "stroke-opacity"); DocumentUndo::maybeDone(_desktop->getDocument(), (kind == FILL) ? undo_F_label : undo_S_label, (kind == FILL) ? _("Set fill color") : _("Set stroke color"), INKSCAPE_ICON("dialog-fill-and-stroke")); - if (kind == FILL) { - // resume interruptibility - _desktop->getCanvas()->forced_redraws_stop(); - } - // on release, toggle undo_label so that the next drag will not be lumped with this one if (undo_F_label == undo_F_label_1) { undo_F_label = undo_F_label_2; diff --git a/src/ui/widget/selected-style.cpp b/src/ui/widget/selected-style.cpp index 1a4be9dfda..433112c799 100644 --- a/src/ui/widget/selected-style.cpp +++ b/src/ui/widget/selected-style.cpp @@ -1147,21 +1147,9 @@ void SelectedStyle::on_opacity_changed () Inkscape::CSSOStringStream os; os << CLAMP ((_opacity_adjustment->get_value() / 100), 0.0, 1.0); sp_repr_css_set_property (css, "opacity", os.str().c_str()); - - // FIXME: workaround for GTK breakage: display interruptibility sometimes results in GTK - // sending multiple value-changed events. As if when Inkscape interrupts redraw for main loop - // iterations, GTK discovers that this callback hasn't finished yet, and for some weird reason - // decides to add yet another value-changed event to the queue. Totally braindead if you ask - // me. As a result, scrolling the spinbutton once results in runaway change until it hits 1.0 - // or 0.0. (And no, this is not a race with ::update, I checked that.) - // Sigh. So we disable interruptibility while we're setting the new value. - _desktop->getCanvas()->forced_redraws_start(0); sp_desktop_set_style (_desktop, css); sp_repr_css_attr_unref (css); DocumentUndo::maybeDone(_desktop->getDocument(), "fillstroke:opacity", _("Change opacity"), INKSCAPE_ICON("dialog-fill-and-stroke")); - - // resume interruptibility - _desktop->getCanvas()->forced_redraws_stop(); _opacity_blocked = false; } -- GitLab From ae72ee758af31176b762bd605809a986e2dd7f89 Mon Sep 17 00:00:00 2001 From: PBS Date: Fri, 21 Jan 2022 09:55:41 +0900 Subject: [PATCH 20/35] Add responsive and full-redraw updaters. This is a warm-up for the multi-scale updater. This work supersedes forced redraws; removal initiated. --- src/ui/widget/canvas-grid.cpp | 4 +- src/ui/widget/canvas.cpp | 240 ++++++++++++++++++++++------------ src/ui/widget/canvas.h | 5 - 3 files changed, 161 insertions(+), 88 deletions(-) diff --git a/src/ui/widget/canvas-grid.cpp b/src/ui/widget/canvas-grid.cpp index 770ae2f8de..cf3f819b1e 100644 --- a/src/ui/widget/canvas-grid.cpp +++ b/src/ui/widget/canvas-grid.cpp @@ -154,8 +154,8 @@ void CanvasGrid::UpdateRulers() { Geom::Rect viewbox = _dtw->desktop->get_display_area().bounds(); - // "true" means: Use integer values of the canvas for calculating the display area, similar - // to the integer values used for positioning the grid lines. (see SPCanvas::scrollTo(), + // Use integer values of the canvas for calculating the display area, similar + // to the integer values used for positioning the grid lines. (see Canvas::scrollTo(), // where ix and iy are rounded integer values; these values are stored in CanvasItemBuffer->rect, // and used for drawing the grid). By using the integer values here too, the ruler ticks // will be perfectly aligned to the grid diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 0952fef2ea..bfd5a0d807 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -93,10 +93,10 @@ namespace UI { namespace Widget { struct PaintRectSetup { + Cairo::RefPtr clean_region; Geom::IntPoint mouse_loc; int max_pixels; gint64 start_time; - bool disable_timeouts; }; // Preferences system @@ -178,6 +178,131 @@ struct Prefs Pref debug_sticky_decoupled = Pref ("/options/rendering/debug/sticky_decoupled"); }; +// Conversion functions + +auto geom_to_cairo(Geom::IntRect rect) +{ + return Cairo::RectangleInt{rect.left(), rect.top(), rect.width(), rect.height()}; +} + +auto cairo_to_geom(Cairo::RectangleInt rect) +{ + return Geom::IntRect::from_xywh(rect.x, rect.y, rect.width, rect.height); +} + +auto geom_to_cairo(Geom::Affine affine) +{ + return Cairo::Matrix(affine[0], affine[1], affine[2], affine[3], affine[4], affine[5]); +} + +auto geom_act(Geom::Affine a, Geom::IntPoint p) +{ + Geom::Point p2 = p; + p2 *= a; + return Geom::IntPoint(std::round(p2.x()), std::round(p2.y())); +} + +void region_to_path(const Cairo::RefPtr &cr, const Cairo::RefPtr ®) +{ + for (int i = 0; i < reg->get_num_rectangles(); i++) { + auto rect = reg->get_rectangle(i); + cr->rectangle(rect.x, rect.y, rect.width, rect.height); + } +} + +// Update strategy + +enum UpdateStrategy { + Responsive, + FullRedraw, + Multiscale +}; + +// A class for controlling when invalidated regions become available for redraw. +class Updater +{ +public: + Cairo::RefPtr clean_region; + + Updater(Cairo::RefPtr clean_region) : clean_region(clean_region) {} + + virtual void reset() {clean_region = Cairo::Region::create();} + virtual void intersect(const Geom::IntRect &rect) {clean_region->intersect(geom_to_cairo(rect));} + virtual void mark_dirty(const Geom::IntRect &rect) {clean_region->subtract(geom_to_cairo(rect));} + virtual void mark_clean(const Geom::IntRect &rect) {clean_region->do_union(geom_to_cairo(rect));} + + virtual Cairo::RefPtr checkout_clean_region() {return clean_region;}; + virtual bool report_completed() {return false;} +}; + +// Responsive/naive updater: As soon as a region is invalidated, redraw it. +using ResponsiveUpdater = Updater; + +// Full-redraw updater: When a region is invalidated, delay redraw until after the current redraw is completed. +class FullredrawUpdater : public Updater +{ + bool checkedout; + Cairo::RefPtr frozen; + +public: + FullredrawUpdater(Cairo::RefPtr clean_region) : Updater(clean_region), checkedout(false) {} + + void reset() override + { + Updater::reset(); + checkedout = false; + frozen.clear(); + } + + void intersect(const Geom::IntRect &rect) override + { + Updater::intersect(rect); + if (frozen) frozen->intersect(geom_to_cairo(rect)); + } + + void mark_dirty(const Geom::IntRect &rect) override + { + if (checkedout && !frozen) frozen = clean_region->copy(); // CoW triggered + Updater::mark_dirty(rect); + } + + void mark_clean(const Geom::IntRect &rect) override + { + Updater::mark_clean(rect); + if (frozen) frozen->do_union(geom_to_cairo(rect)); + } + + Cairo::RefPtr checkout_clean_region() override + { + if (!frozen) { + checkedout = true; + return clean_region; + } else { + return frozen; + } + } + + bool report_completed() override + { + assert(checkedout); + if (!frozen) { + checkedout = false; + return false; + } else { + frozen.clear(); + return true; + } + } +}; + +class MultiscaleUpdater : public Updater +{ +public: + MultiscaleUpdater(Cairo::RefPtr clean_region) : Updater(clean_region) {} +}; + +// Implementation class + class CanvasPrivate { public: @@ -198,7 +323,8 @@ public: Geom::IntRect _store_rect; ///< Rectangle of the store in world space. Geom::Affine _store_affine; Cairo::RefPtr _backing_store; ///< Canvas content. - Cairo::RefPtr _clean_region; ///< Subregion of backing store with up-to-date content. + + std::unique_ptr updater; // Holds the clean region, the subregion of the store with up-to-date content, and decides in what order the unclean regions should be redrawn. Geom::IntRect _snapshot_rect; Geom::Affine _snapshot_affine; @@ -244,7 +370,7 @@ void CanvasPrivate::schedule_bucket_emptier() bucket_emptier.disconnect(); empty_bucket(); return false; - }, G_PRIORITY_DEFAULT_IDLE - 5); // before lowpri_idle + }, G_PRIORITY_DEFAULT_IDLE - 5); // before lopri_idle } void CanvasPrivate::empty_bucket() @@ -325,7 +451,7 @@ Canvas::Canvas() _pick_event.crossing.y = 0; // Drawing - d->_clean_region = Cairo::Region::create(); + d->updater = std::make_unique(Cairo::Region::create()); _background = Cairo::SolidPattern::create_rgb(1.0, 1.0, 1.0); d->solid_background = true; @@ -409,7 +535,7 @@ Canvas::redraw_all() // We need to ignore their requests! return; } - d->_clean_region = Cairo::Region::create(); // Empty region (i.e. everything is dirty). + d->updater->reset(); // Empty region (i.e. everything is dirty). d->add_idle(); if (d->prefs.debug_show_unclean) queue_draw(); } @@ -441,8 +567,8 @@ Canvas::redraw_area(int x0, int y0, int x1, int y1) x1 = std::clamp(x1, min_coord, max_coord); y1 = std::clamp(y1, min_coord, max_coord); - Cairo::RectangleInt crect = { x0, y0, x1-x0, y1-y0 }; - d->_clean_region->subtract(crect); + auto rect = Geom::IntRect::from_xywh(x0, y0, x1-x0, y1-y0); + d->updater->mark_dirty(rect); d->add_idle(); if (d->prefs.debug_show_unclean) queue_draw(); } @@ -586,10 +712,7 @@ Cairo::RefPtr Canvas::get_backing_store() const void Canvas::forced_redraws_start(int count, bool reset) { - _forced_redraw_limit = count; - if (reset) { - _forced_redraw_count = 0; - } + // Todo: Likely to be removed, depending on feedback. } /** @@ -904,39 +1027,6 @@ void Canvas::on_realize() d->add_idle(); } -auto -geom_to_cairo(Geom::IntRect rect) -{ - return Cairo::RectangleInt{rect.left(), rect.top(), rect.width(), rect.height()}; -} - -auto -cairo_to_geom(Cairo::RectangleInt rect) -{ - return Geom::IntRect::from_xywh(rect.x, rect.y, rect.width, rect.height); -} - -auto geom_to_cairo(Geom::Affine affine) -{ - return Cairo::Matrix(affine[0], affine[1], affine[2], affine[3], affine[4], affine[5]); -} - -auto geom_act(Geom::Affine a, Geom::IntPoint p) -{ - Geom::Point p2 = p; - p2 *= a; - return Geom::IntPoint(std::round(p2.x()), std::round(p2.y())); -} - -void -region_to_path(const Cairo::RefPtr &cr, const Cairo::RefPtr ®) -{ - for (int i = 0; i < reg->get_num_rectangles(); i++) { - auto rect = reg->get_rectangle(i); - cr->rectangle(rect.x, rect.y, rect.width, rect.height); - } -} - /* * The on_draw() function is called whenever Gtk wants to update the window. This function: * @@ -1002,7 +1092,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->rectangle(0, 0, get_allocation().get_width(), get_allocation().get_height()); cr->translate(-_x0, -_y0); cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); - region_to_path(cr, d->_clean_region); + region_to_path(cr, d->updater->clean_region); cr->transform(geom_to_cairo(d->_store_affine * d->_snapshot_affine.inverse())); region_to_path(cr, d->_snapshot_clean_region); cr->clip(); @@ -1020,7 +1110,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->rectangle(0, 0, get_allocation().get_width(), get_allocation().get_height()); cr->translate(-_x0, -_y0); cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); - region_to_path(cr, d->_clean_region); + region_to_path(cr, d->updater->clean_region); cr->clip(); cr->transform(geom_to_cairo(d->_store_affine * d->_snapshot_affine.inverse())); region_to_path(cr, d->_snapshot_clean_region); @@ -1041,7 +1131,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->save(); cr->translate(-_x0, -_y0); cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); - region_to_path(cr, d->_clean_region); + region_to_path(cr, d->updater->clean_region); cr->clip(); cr->set_source(d->_backing_store, d->_store_rect.left(), d->_store_rect.top()); cr->set_operator(d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); @@ -1054,7 +1144,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) if (d->prefs.debug_show_unclean) { if (d->prefs.debug_framecheck) f = FrameCheck::Event("paint_unclean"); auto reg = Cairo::Region::create(geom_to_cairo(d->_store_rect)); - reg->subtract(d->_clean_region); + reg->subtract(d->updater->clean_region); cr->save(); cr->translate(-_x0, -_y0); if (d->decoupled_mode) { @@ -1075,7 +1165,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); } cr->set_source_rgba(0, 0.7, 0, 0.4); - region_to_path(cr, d->_clean_region); + region_to_path(cr, d->updater->clean_region); cr->stroke(); cr->restore(); } @@ -1260,7 +1350,7 @@ CanvasPrivate::on_idle() cr->set_operator(Cairo::OPERATOR_CLEAR); } cr->paint(); - _clean_region = Cairo::Region::create(); + updater->reset(); if (prefs.debug_show_unclean) q->queue_draw(); }; @@ -1319,7 +1409,7 @@ CanvasPrivate::on_idle() _store_rect = store_rect; assert(_store_affine == q->_affine); // Should not be called if the affine has changed. _backing_store = std::move(backing_store); - _clean_region->intersect(geom_to_cairo(_store_rect)); + updater->intersect(_store_rect); if (prefs.debug_show_unclean) q->queue_draw(); }; @@ -1328,7 +1418,7 @@ CanvasPrivate::on_idle() std::swap(_snapshot_store, _backing_store); // This will re-use the old snapshot store later if possible. _snapshot_rect = _store_rect; _snapshot_affine = _store_affine; - _snapshot_clean_region = _clean_region->copy(); + _snapshot_clean_region = updater->clean_region->copy(); // Recreate the backing store, making the state valid again. recreate_store(); @@ -1386,7 +1476,7 @@ CanvasPrivate::on_idle() // Assert that _clean_region is a subregion of _store_rect. #ifndef NDEBUG - auto tmp = _clean_region->copy(); + auto tmp = updater->clean_region->copy(); tmp->subtract(geom_to_cairo(_store_rect)); assert(tmp->empty()); #endif @@ -1421,7 +1511,7 @@ CanvasPrivate::on_idle() // The visible rect is the intersection of this with the store visible_rect = bi & _store_rect; - // Note: We could have used a smaller region containing pl consisting of many rectangles, rather than a single bounding box. + // Note: We could have used a smaller region containing of consisting of many rectangles, rather than a single bounding box. // However, while this uses less pixels it also uses more rectangles, so is NOT necessarily an optimisation and could backfire. // For this reason, and for simplicity, we use the bounding rect. (Note: Slightly invalidated by addition of coarsener below.) } @@ -1432,7 +1522,7 @@ CanvasPrivate::on_idle() Cairo::RefPtr paint_region; if (visible_rect) { paint_region = Cairo::Region::create(geom_to_cairo(*visible_rect)); - paint_region->subtract(_clean_region); + paint_region->subtract(updater->checkout_clean_region()); } else { paint_region = Cairo::Region::create(); } @@ -1441,7 +1531,7 @@ CanvasPrivate::on_idle() auto paint_rects = coarsen(paint_region, prefs.coarsener_min_size, prefs.coarsener_glue_size); // Clip the coarsened rectangles back to the visible rect, in case coarsening made them go outside it. - // Todo: That should be impossible, but somehow it occasionally happens. Understand! + // Todo: That should be impossible, but somehow it occasionally happens. Or was that in a bad dream? Understand! for (auto it = paint_rects.begin(); it != paint_rects.end(); ) { auto opt = *it & *visible_rect; if (opt) { @@ -1474,6 +1564,7 @@ CanvasPrivate::on_idle() // Todo: Consider further subdividing the region if the mouse lies on the edge of a big rectangle. // Otherwise, the whole of the big rectangle will be rendered first, at the expense of points in other // rectangles very near to the mouse. + // Todo: Will be fixed by non-recursive heap-based bisector soon. // Sort the rectangles to paint by distance from mouse. std::sort(paint_rects.begin(), paint_rects.end(), [&] (const Geom::IntRect &a, const Geom::IntRect &b) { @@ -1482,6 +1573,7 @@ CanvasPrivate::on_idle() // Set up painting info to pass down the stack. PaintRectSetup setup; + setup.clean_region = updater->checkout_clean_region(); setup.mouse_loc = mouse_loc; // Todo: Logging reveals that Inkscape often underestimates render times, and can sometimes time out quite badly, // leading to missed frames. Consider fixing the time estimator below, possibly based on run-time feedback. May @@ -1495,10 +1587,6 @@ CanvasPrivate::on_idle() setup.max_pixels = 262144; } setup.start_time = g_get_monotonic_time(); - //setup.disable_timeouts = _forced_redraw_limit != -1 && _forced_redraw_count >= _forced_redraw_limit; // Enable a forced redraw if asked. - // Todo: Forced redraws are temporarily disabled. They are no longer helpful in all the situations they used to be. - // Need to go through all places where forced redraw is requested, and check whether it is helpful or harmful. - setup.disable_timeouts = false; // If asked to, don't paint anything and instead halt the idle process. if (prefs.debug_disable_redraw) { @@ -1515,26 +1603,18 @@ CanvasPrivate::on_idle() // Timed out. Temporarily return to GTK main loop, and come back here when next idle. if (prefs.debug_logging) std::cout << "Timed out: " << g_get_monotonic_time() - setup.start_time << " us" << std::endl; framecheckobj.subtype = 1; - q->_forced_redraw_count++; return true; } } - // Check if suppressed a timeout. - if (setup.disable_timeouts) { - auto now = g_get_monotonic_time(); - auto elapsed = now - setup.start_time; - if (elapsed > prefs.render_time_limit) { - // Timed out. Reset counter. It will count up again on future timeouts, eventually triggering another forced redraw. - q->_forced_redraw_count = 0; - if (prefs.debug_logging) std::cout << "Ignored timeout: " << g_get_monotonic_time() - setup.start_time << " us" << std::endl; - framecheckobj.subtype = 2; - return false; - } + // Finished redrawing the region requested by the updater. Check if it has any subsequent requests. + if (updater->report_completed()) { + // There is another request; continue redrawing. + return true; // Todo: Would be more logical to jump back to the required point, rather than returning. } // Implement the sticky decoupled mode debug switch. - if (decoupled_mode && prefs.debug_sticky_decoupled) return false; + if (prefs.debug_sticky_decoupled && decoupled_mode) return false; // Finished drawing. Handle transitions out of decoupled mode, by checking if we need to do a final redraw at the correct affine. if (decoupled_mode) { @@ -1577,7 +1657,7 @@ CanvasPrivate::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect co // Due to coarsening, it's possible for bisected rectangles to lie entirely inside the clean region. These can be discarded, and in fact must be, // because otherwise we risk rendering only clean rectangles for the whole frame, which would lead to a render stall. - if (_clean_region->contains_rectangle(geom_to_cairo(this_rect)) == Cairo::REGION_OVERLAP_IN) { + if (setup.clean_region->contains_rectangle(geom_to_cairo(this_rect)) == Cairo::REGION_OVERLAP_IN) { // Rectangle is already clean. return true; } @@ -1676,7 +1756,7 @@ CanvasPrivate::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect co if (prefs.debug_slow_redraw) g_usleep(prefs.debug_slow_redraw_time); // Mark the rectangle as clean. - _clean_region->do_union(geom_to_cairo(this_rect)); + updater->mark_clean(this_rect); // Mark the screen dirty. if (!decoupled_mode) { @@ -1708,12 +1788,10 @@ CanvasPrivate::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect co } // Exit if timed out. - if (!setup.disable_timeouts) { - auto now = g_get_monotonic_time(); - auto elapsed = now - setup.start_time; - if (elapsed > prefs.render_time_limit) { - return false; - } + auto now = g_get_monotonic_time(); + auto elapsed = now - setup.start_time; + if (elapsed > prefs.render_time_limit) { + return false; } // Continue rendering. diff --git a/src/ui/widget/canvas.h b/src/ui/widget/canvas.h index cff71b6770..3f6864bcde 100644 --- a/src/ui/widget/canvas.h +++ b/src/ui/widget/canvas.h @@ -16,14 +16,9 @@ #endif #include - #include - #include <2geom/rect.h> #include <2geom/int-rect.h> - -#include "preferences.h" // Update canvas_item_ctrl sizes. - #include "display/rendermode.h" class SPDesktop; -- GitLab From b480c12ec671f6ba8b1d312ed508730917950ac1 Mon Sep 17 00:00:00 2001 From: PBS Date: Sat, 22 Jan 2022 02:37:09 +0900 Subject: [PATCH 21/35] Multi-scale Updater, ability to choose Updater in Preferences. Also enabled finer bisection by default, since new update strategies work around the issues this had before. --- src/ui/dialog/inkscape-preferences.cpp | 10 +- src/ui/dialog/inkscape-preferences.h | 1 + src/ui/widget/canvas.cpp | 184 ++++++++++++++++++------- 3 files changed, 142 insertions(+), 53 deletions(-) diff --git a/src/ui/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp index 9cc5caed93..6b3148c692 100644 --- a/src/ui/dialog/inkscape-preferences.cpp +++ b/src/ui/dialog/inkscape-preferences.cpp @@ -2705,9 +2705,9 @@ void InkscapePreferences::initPageRendering() _page_rendering.add_line(true, "", _canvas_debug_framecheck, "", _("Print profiling data of selected operations to a file")); _canvas_debug_logging.init(_("Logging"), "/options/rendering/debug/logging", false); _page_rendering.add_line(true, "", _canvas_debug_logging, "", _("Log certain events to the console")); - _canvas_debug_overbisection.init(_("Overbisection"), "/options/rendering/debug/overbisection", false); + _canvas_debug_overbisection.init(_("Overbisection"), "/options/rendering/debug/overbisection", true); _page_rendering.add_line(true, "", _canvas_debug_overbisection, "", _("Bisect all tiles until they reach a minimum size")); - _canvas_debug_overbisection_size.init("/options/rendering/debug/overbisection_size", 1.0, 10000.0, 1.0, 0.0, 30.0, true, false); + _canvas_debug_overbisection_size.init("/options/rendering/debug/overbisection_size", 1.0, 10000.0, 1.0, 0.0, 400.0, true, false); _page_rendering.add_line(true, _("Overbisection size"), _canvas_debug_overbisection_size, C_("pixel abbreviation", "px"), _("The maxmimum allowed tile size"), false); _canvas_debug_slow_redraw.init(_("Slow redraw"), "/options/rendering/debug/slow_redraw", false); _page_rendering.add_line(true, "", _canvas_debug_slow_redraw, "", _("Introduce a fixed delay for each tile")); @@ -2726,7 +2726,13 @@ void InkscapePreferences::initPageRendering() _canvas_debug_sticky_decoupled.init(_("Sticky decoupled mode"), "/options/rendering/debug/sticky_decoupled", false); _page_rendering.add_line(true, "", _canvas_debug_sticky_decoupled, "", _("Stay in decoupled mode even after rendering is complete")); + // For update strategy. + int values[] = {1, 2, 3}; + Glib::ustring labels[] = {_("Responsive"), _("Full redraw"), _("Multiscale")}; + _page_rendering.add_group_header(_("Low-level tuning options")); + _canvas_update_strategy.init("/options/rendering/update_strategy", labels, values, 3, 3); + _page_rendering.add_line(true, _("Update strategy:"), _canvas_update_strategy, "", _("What order to update stale content when it cannot be drawn fast enough."), false); _canvas_render_time_limit.init("/options/rendering/render_time_limit", 100.0, 1000000.0, 1.0, 0.0, 1000.0, true, false); _page_rendering.add_line(true, _("Render time limit"), _canvas_render_time_limit, C_("microsecond abbreviation", "μs"), _("The maximum time allowed for a rendering time slice"), false); _canvas_max_affine_diff.init("/options/rendering/max_affine_diff", 0.0, 100.0, 0.1, 0.0, 1.8, false, false); diff --git a/src/ui/dialog/inkscape-preferences.h b/src/ui/dialog/inkscape-preferences.h index 2dfe449f79..de9a3df02e 100644 --- a/src/ui/dialog/inkscape-preferences.h +++ b/src/ui/dialog/inkscape-preferences.h @@ -350,6 +350,7 @@ protected: UI::Widget::PrefSpinButton _canvas_debug_overbisection_size; UI::Widget::PrefSpinButton _canvas_debug_slow_redraw_time; + UI::Widget::PrefCombo _canvas_update_strategy; UI::Widget::PrefSpinButton _canvas_render_time_limit; UI::Widget::PrefSpinButton _canvas_max_affine_diff; UI::Widget::PrefSpinButton _canvas_pad; diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index bfd5a0d807..329185ae51 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -106,6 +106,7 @@ struct PrefBase { T t; std::unique_ptr obs; + std::function action; operator T() const {return t;} }; @@ -115,24 +116,22 @@ struct Pref {}; template<> struct Pref : PrefBase { - std::function action; - Pref(const char *path) + Pref(const char *path, bool def = false) { auto prefs = Inkscape::Preferences::get(); - t = prefs->getBool(path); - obs = prefs->createObserver(path, [=] (const Preferences::Entry &e) {t = e.getBool(); if (action) action();}); + t = prefs->getBool(path, def); + obs = prefs->createObserver(path, [=] (const Preferences::Entry &e) {t = e.getBool(def); if (action) action();}); } }; template<> struct Pref : PrefBase { - std::function action; Pref(const char *path, int def, int min, int max) { auto prefs = Inkscape::Preferences::get(); t = prefs->getIntLimited(path, def, min, max); - obs = prefs->createObserver(path, [=] (const Preferences::Entry &e) {t = e.getIntLimited(def, min, max); if (action) action(t);}); + obs = prefs->createObserver(path, [=] (const Preferences::Entry &e) {t = e.getIntLimited(def, min, max); if (action) action();}); } }; @@ -143,7 +142,7 @@ struct Pref : PrefBase { auto prefs = Inkscape::Preferences::get(); t = prefs->getDoubleLimited(path, def, min, max); - obs = prefs->createObserver(path, [=] (const Preferences::Entry &e) {t = e.getDoubleLimited(def, min, max);}); + obs = prefs->createObserver(path, [=] (const Preferences::Entry &e) {t = e.getDoubleLimited(def, min, max); if (action) action();}); } }; @@ -157,6 +156,7 @@ struct Prefs Pref grabsize = Pref ("/options/grabsize/value", 3, 1, 15); // New parameters + Pref update_strategy = Pref ("/options/rendering/update_strategy", 3, 1, 3); Pref render_time_limit = Pref ("/options/rendering/render_time_limit", 1000, 100, 1000000); Pref max_affine_diff = Pref("/options/rendering/max_affine_diff", 1.8, 0.0, 100.0); Pref pad = Pref ("/options/rendering/pad", 200, 0, 1000); @@ -166,8 +166,8 @@ struct Prefs // Debug switches Pref debug_framecheck = Pref ("/options/rendering/debug/framecheck"); Pref debug_logging = Pref ("/options/rendering/debug/logging"); - Pref debug_overbisection = Pref ("/options/rendering/debug/overbisection"); - Pref debug_overbisection_size = Pref ("/options/rendering/debug/overbisection_size", 30, 1, 10000); + Pref debug_overbisection = Pref ("/options/rendering/debug/overbisection", true); + Pref debug_overbisection_size = Pref ("/options/rendering/debug/overbisection_size", 400, 1, 10000); Pref debug_slow_redraw = Pref ("/options/rendering/debug/slow_redraw"); Pref debug_slow_redraw_time = Pref ("/options/rendering/debug/slow_redraw_time", 50, 0, 1000000); Pref debug_show_redraw = Pref ("/options/rendering/debug/show_redraw"); @@ -212,95 +212,176 @@ void region_to_path(const Cairo::RefPtr &cr, const Cairo::RefPtr // Update strategy -enum UpdateStrategy { - Responsive, - FullRedraw, - Multiscale -}; - -// A class for controlling when invalidated regions become available for redraw. +// A class for controlling what order to update invalidated regions. class Updater { public: + // The subgregion of the store with up-to-date content. Cairo::RefPtr clean_region; Updater(Cairo::RefPtr clean_region) : clean_region(clean_region) {} - virtual void reset() {clean_region = Cairo::Region::create();} - virtual void intersect(const Geom::IntRect &rect) {clean_region->intersect(geom_to_cairo(rect));} - virtual void mark_dirty(const Geom::IntRect &rect) {clean_region->subtract(geom_to_cairo(rect));} - virtual void mark_clean(const Geom::IntRect &rect) {clean_region->do_union(geom_to_cairo(rect));} + virtual void reset() {clean_region = Cairo::Region::create();} // Reset to clean region to empty. + virtual void intersect (const Geom::IntRect &rect) {clean_region->intersect(geom_to_cairo(rect));} // Called when the store changes position to clip everthing to the new store rect. + virtual void mark_dirty(const Geom::IntRect &rect) {clean_region->subtract (geom_to_cairo(rect));} // Called on every invalidate event. + virtual void mark_clean(const Geom::IntRect &rect) {clean_region->do_union (geom_to_cairo(rect));} // Called on every rectangle redrawn. - virtual Cairo::RefPtr checkout_clean_region() {return clean_region;}; - virtual bool report_completed() {return false;} + virtual Cairo::RefPtr get_next_clean_region() {return clean_region;}; // Called once in on_idle to determine what regions to consider clean for the current draw. + virtual bool finished_drawing () {return false;} // Called in on_idle if drawing has finished. Returns true to indicate that drawing should continue, because the next clean region will be different. + virtual void frame () {} // Called by on_draw to notify the end of drawing and the display of the frame. }; -// Responsive/naive updater: As soon as a region is invalidated, redraw it. +// Responsive updater: As soon as a region is invalidated, redraw it. using ResponsiveUpdater = Updater; -// Full-redraw updater: When a region is invalidated, delay redraw until after the current redraw is completed. +// Full redraw updater: When a region is invalidated, delay redraw until after the current redraw is completed. class FullredrawUpdater : public Updater { - bool checkedout; - Cairo::RefPtr frozen; + // Whether we are currently in the middle of a redraw. + bool inprogress; + + // If more damage events arrive while a redraw is in progress, save the clean region to here so we can continue the redraw using it. + Cairo::RefPtr old_clean_region; public: - FullredrawUpdater(Cairo::RefPtr clean_region) : Updater(clean_region), checkedout(false) {} + FullredrawUpdater(Cairo::RefPtr clean_region) : Updater(clean_region), inprogress(false) {} void reset() override { Updater::reset(); - checkedout = false; - frozen.clear(); + inprogress = false; + old_clean_region.clear(); } void intersect(const Geom::IntRect &rect) override { Updater::intersect(rect); - if (frozen) frozen->intersect(geom_to_cairo(rect)); + if (old_clean_region) old_clean_region->intersect(geom_to_cairo(rect)); } void mark_dirty(const Geom::IntRect &rect) override { - if (checkedout && !frozen) frozen = clean_region->copy(); // CoW triggered + if (inprogress && !old_clean_region) old_clean_region = clean_region->copy(); // CoW Updater::mark_dirty(rect); } void mark_clean(const Geom::IntRect &rect) override { Updater::mark_clean(rect); - if (frozen) frozen->do_union(geom_to_cairo(rect)); + if (old_clean_region) old_clean_region->do_union(geom_to_cairo(rect)); } - Cairo::RefPtr checkout_clean_region() override + Cairo::RefPtr get_next_clean_region() override { - if (!frozen) { - checkedout = true; + // Return the old clean region if it was saved, otherwise return the current one and set a flag indicating redraw in progress. + if (!old_clean_region) { + inprogress = true; return clean_region; } else { - return frozen; + return old_clean_region; } } - bool report_completed() override + bool finished_drawing() override { - assert(checkedout); - if (!frozen) { - checkedout = false; + assert(inprogress); + if (!old_clean_region) { + // Redrawn using the actual clean region --> finished. + inprogress = false; return false; } else { - frozen.clear(); + // Redrawn using the old clean region; new clean region has been damaged --> draw again using the new clean region. + old_clean_region.clear(); return true; } } }; +// Multiscale updater: Updates tiles near the mouse faster. Gives the best of both. class MultiscaleUpdater : public Updater { + int counter, depth, counter2; + std::vector> blocked; + public: - MultiscaleUpdater(Cairo::RefPtr clean_region) : Updater(clean_region) {} + MultiscaleUpdater(Cairo::RefPtr clean_region) : Updater(clean_region), counter(0), depth(0), counter2(0), blocked({Cairo::Region::create()}) {} + + void reset() override + { + Updater::reset(); + counter = depth = counter2 = 0; + blocked = {Cairo::Region::create()}; + } + + void intersect(const Geom::IntRect &rect) override + { + Updater::intersect(rect); + for (auto ® : blocked) { + reg->intersect(geom_to_cairo(rect)); + } + } + + void mark_clean(const Geom::IntRect &rect) override + { + Updater::mark_clean(rect); + blocked[depth]->do_union(geom_to_cairo(rect)); + } + + Cairo::RefPtr get_next_clean_region() override + { + auto result = clean_region->copy(); + result->do_union(blocked[depth]); + return result; + } + + bool finished_drawing() override + { + // We have finished work if we have completed a redraw using the depth 0 clean region, which is always the true clean region. + bool ret = depth > 0; + // Perform a reset of the updating sequence in both cases. + counter = depth = counter2 = 0; + blocked = {Cairo::Region::create()}; + return ret; + } + + void frame() override + { + // Stay at the current depth for 2^depth frames. + counter2++; + if (counter2 < (1 << depth)) return; + counter2 = 0; + + // Adjust the counter, which causes depth to hop around the values 0, 1, 2... spending half as much time at each subsequent depth. + counter++; + depth = 0; + for (int tmp = counter; tmp % 2 == 1; tmp /= 2) { + depth++; + } + + // Ensure enough blocked zones exist. + if (depth == blocked.size()) { + blocked.emplace_back(); + } + + // Recreate the current blocked zone as the union of those previous to it. + blocked[depth] = Cairo::Region::create(); + for (int i = 0; i < depth; i++) { + blocked[depth]->do_union(blocked[i]); + } + } }; +std::unique_ptr +make_updater(int type, Cairo::RefPtr clean_region = Cairo::Region::create()) +{ + switch (type) { + case 1: return std::make_unique(clean_region); + case 2: return std::make_unique(clean_region); + case 3: return std::make_unique(clean_region); + default: assert(false); + } +} + // Implementation class class CanvasPrivate @@ -439,11 +520,12 @@ Canvas::Canvas() Gdk::SMOOTH_SCROLL_MASK ); // Preferences - d->prefs.grabsize.action = [=] (int size) {_canvas_item_root->update_canvas_item_ctrl_sizes(size);}; + d->prefs.grabsize.action = [=] {_canvas_item_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 = [=] {queue_draw();}; d->prefs.debug_sticky_decoupled.action = [=] {d->add_idle();}; + d->prefs.update_strategy.action = [=] {d->updater = make_updater(d->prefs.update_strategy, d->updater->clean_region);}; // Give _pick_event an initial definition. _pick_event.type = GDK_LEAVE_NOTIFY; @@ -451,7 +533,7 @@ Canvas::Canvas() _pick_event.crossing.y = 0; // Drawing - d->updater = std::make_unique(Cairo::Region::create()); + d->updater = make_updater(d->prefs.update_strategy); _background = Cairo::SolidPattern::create_rgb(1.0, 1.0, 1.0); d->solid_background = true; @@ -1181,6 +1263,9 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) // Todo: Add back X-ray view. + // Notify the update strategy that another frame has passed. + d->updater->frame(); + return true; } @@ -1519,10 +1604,11 @@ CanvasPrivate::on_idle() assert(_store_rect.contains(visible_rect)); // Get the region to paint, which is the visible rectangle minus the clean region (both subregions of store). + Cairo::RefPtr clean_region = updater->get_next_clean_region(); Cairo::RefPtr paint_region; if (visible_rect) { paint_region = Cairo::Region::create(geom_to_cairo(*visible_rect)); - paint_region->subtract(updater->checkout_clean_region()); + paint_region->subtract(clean_region); } else { paint_region = Cairo::Region::create(); } @@ -1573,12 +1659,8 @@ CanvasPrivate::on_idle() // Set up painting info to pass down the stack. PaintRectSetup setup; - setup.clean_region = updater->checkout_clean_region(); + setup.clean_region = clean_region; setup.mouse_loc = mouse_loc; - // Todo: Logging reveals that Inkscape often underestimates render times, and can sometimes time out quite badly, - // leading to missed frames. Consider fixing the time estimator below, possibly based on run-time feedback. May - // even wish to consider maintaining a coarse heatmap of the drawing to judge rendering time/order. In the meantime, - // this can be worked around by enabling overbisection with a tile size of about 400. if (q->_render_mode != Inkscape::RenderMode::OUTLINE) { // Can't be too small or large gradient will be rerendered too many times! setup.max_pixels = 65536 * prefs.tile_multiplier; @@ -1608,7 +1690,7 @@ CanvasPrivate::on_idle() } // Finished redrawing the region requested by the updater. Check if it has any subsequent requests. - if (updater->report_completed()) { + if (updater->finished_drawing()) { // There is another request; continue redrawing. return true; // Todo: Would be more logical to jump back to the required point, rather than returning. } -- GitLab From c58422451b6c859044f16588af90445a92b05c7a Mon Sep 17 00:00:00 2001 From: PBS Date: Sat, 22 Jan 2022 07:38:32 +0900 Subject: [PATCH 22/35] Fix assert(false) not behaving like I expect. --- src/ui/widget/canvas.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 329185ae51..b568d86700 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -377,8 +377,8 @@ make_updater(int type, Cairo::RefPtr clean_region = Cairo::Region switch (type) { case 1: return std::make_unique(clean_region); case 2: return std::make_unique(clean_region); + default: case 3: return std::make_unique(clean_region); - default: assert(false); } } -- GitLab From 53116069954b241bee1c62ab70b0945567476eb8 Mon Sep 17 00:00:00 2001 From: PBS Date: Sat, 22 Jan 2022 18:49:18 +0900 Subject: [PATCH 23/35] Use heap-based instead of recursive bisector. Actually processes things in the right order. Also re-splitting of code across functions for much cleanup. --- src/ui/dialog/inkscape-preferences.cpp | 58 ++-- src/ui/dialog/inkscape-preferences.h | 20 +- src/ui/widget/canvas.cpp | 393 +++++++++++-------------- 3 files changed, 209 insertions(+), 262 deletions(-) diff --git a/src/ui/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp index 6b3148c692..9f28b6aa30 100644 --- a/src/ui/dialog/inkscape-preferences.cpp +++ b/src/ui/dialog/inkscape-preferences.cpp @@ -2700,41 +2700,17 @@ void InkscapePreferences::initPageRendering() _page_rendering.add_line( true, "", _filter_quality_worst, "", _("Lowest quality (considerable artifacts), but display is fastest")); - _page_rendering.add_group_header(_("Debugging, profiling, and experiments")); - _canvas_debug_framecheck.init(_("Framecheck"), "/options/rendering/debug/framecheck", false); - _page_rendering.add_line(true, "", _canvas_debug_framecheck, "", _("Print profiling data of selected operations to a file")); - _canvas_debug_logging.init(_("Logging"), "/options/rendering/debug/logging", false); - _page_rendering.add_line(true, "", _canvas_debug_logging, "", _("Log certain events to the console")); - _canvas_debug_overbisection.init(_("Overbisection"), "/options/rendering/debug/overbisection", true); - _page_rendering.add_line(true, "", _canvas_debug_overbisection, "", _("Bisect all tiles until they reach a minimum size")); - _canvas_debug_overbisection_size.init("/options/rendering/debug/overbisection_size", 1.0, 10000.0, 1.0, 0.0, 400.0, true, false); - _page_rendering.add_line(true, _("Overbisection size"), _canvas_debug_overbisection_size, C_("pixel abbreviation", "px"), _("The maxmimum allowed tile size"), false); - _canvas_debug_slow_redraw.init(_("Slow redraw"), "/options/rendering/debug/slow_redraw", false); - _page_rendering.add_line(true, "", _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); - _page_rendering.add_line(true, _("Slow redraw time"), _canvas_debug_slow_redraw_time, C_("microsecond abbreviation", "μs"), _("The delay to introduce for each tile"), false); - _canvas_debug_show_redraw.init(_("Show redraw"), "/options/rendering/debug/show_redraw", false); - _page_rendering.add_line(true, "", _canvas_debug_show_redraw, "", _("Paint a translucent random colour over each newly drawn tile")); - _canvas_debug_show_unclean.init(_("Show unclean region"), "/options/rendering/debug/show_unclean", false); - _page_rendering.add_line(true, "", _canvas_debug_show_unclean, "", _("Show the unclean region in red")); - _canvas_debug_show_snapshot.init(_("Show snapshot"), "/options/rendering/debug/show_snapshot", false); - _page_rendering.add_line(true, "", _canvas_debug_show_snapshot, "", _("Show the snapshot region in blue")); - _canvas_debug_show_clean.init(_("Show clean fragmentation"), "/options/rendering/debug/show_clean", false); - _page_rendering.add_line(true, "", _canvas_debug_show_clean, "", _("Show the outlines of the rectangles in the clean region in green")); - _canvas_debug_disable_redraw.init(_("Disable redraw"), "/options/rendering/debug/disable_redraw", false); - _page_rendering.add_line(true, "", _canvas_debug_disable_redraw, "", _("Temporarily disable the idle redraw process completely")); - _canvas_debug_sticky_decoupled.init(_("Sticky decoupled mode"), "/options/rendering/debug/sticky_decoupled", false); - _page_rendering.add_line(true, "", _canvas_debug_sticky_decoupled, "", _("Stay in decoupled mode even after rendering is complete")); - - // For update strategy. + _page_rendering.add_group_header(_("Low-level tuning options")); int values[] = {1, 2, 3}; Glib::ustring labels[] = {_("Responsive"), _("Full redraw"), _("Multiscale")}; - - _page_rendering.add_group_header(_("Low-level tuning options")); _canvas_update_strategy.init("/options/rendering/update_strategy", labels, values, 3, 3); - _page_rendering.add_line(true, _("Update strategy:"), _canvas_update_strategy, "", _("What order to update stale content when it cannot be drawn fast enough."), false); + _page_rendering.add_line(true, _("Update strategy:"), _canvas_update_strategy, "", _("How to update changing content when drawing is not fast enough."), false); _canvas_render_time_limit.init("/options/rendering/render_time_limit", 100.0, 1000000.0, 1.0, 0.0, 1000.0, true, false); _page_rendering.add_line(true, _("Render time limit"), _canvas_render_time_limit, C_("microsecond abbreviation", "μs"), _("The maximum time allowed for a rendering time slice"), false); + _canvas_use_new_bisector.init(_("Use new bisector"), "/options/rendering/use_new_bisector", true); + _page_rendering.add_line(true, "", _canvas_use_new_bisector, "", _("Use an alternative, more obvious bisection strategy: just chop 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, 400.0, true, false); + _page_rendering.add_line(true, _("New bisector tile size"), _canvas_new_bisector_size, C_("pixel abbreviation", "px"), _("Chop rectangles until largest dimension is this small"), false); _canvas_max_affine_diff.init("/options/rendering/max_affine_diff", 0.0, 100.0, 0.1, 0.0, 1.8, false, false); _page_rendering.add_line(true, _("Max affine diff"), _canvas_max_affine_diff, "", _("How much the viewing transformation can change before throwing away the current redraw and starting again"), false); _canvas_pad.init("/options/rendering/pad", 0.0, 1000.0, 1.0, 0.0, 200.0, true, false); @@ -2744,6 +2720,28 @@ void InkscapePreferences::initPageRendering() _canvas_coarsener_glue_size.init("/options/rendering/coarsener_glue_size", 0.0, 1000.0, 1.0, 0.0, 80.0, true, false); _page_rendering.add_line(true, _("Coarsener glue size"), _canvas_coarsener_glue_size, C_("pixel abbreviation", "px"), _("Parameter given to the coarsener algorithm when applied to the paint region. Probably best left alone!"), false); + _page_rendering.add_group_header(_("Debugging, profiling, and experiments")); + _canvas_debug_framecheck.init(_("Framecheck"), "/options/rendering/debug_framecheck", false); + _page_rendering.add_line(true, "", _canvas_debug_framecheck, "", _("Print profiling data of selected operations to a file")); + _canvas_debug_logging.init(_("Logging"), "/options/rendering/debug_logging", false); + _page_rendering.add_line(true, "", _canvas_debug_logging, "", _("Log certain events to the console")); + _canvas_debug_slow_redraw.init(_("Slow redraw"), "/options/rendering/debug_slow_redraw", false); + _page_rendering.add_line(true, "", _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); + _page_rendering.add_line(true, _("Slow redraw time"), _canvas_debug_slow_redraw_time, C_("microsecond abbreviation", "μs"), _("The delay to introduce for each tile"), false); + _canvas_debug_show_redraw.init(_("Show redraw"), "/options/rendering/debug_show_redraw", false); + _page_rendering.add_line(true, "", _canvas_debug_show_redraw, "", _("Paint a translucent random colour over each newly drawn tile")); + _canvas_debug_show_unclean.init(_("Show unclean region"), "/options/rendering/debug_show_unclean", false); + _page_rendering.add_line(true, "", _canvas_debug_show_unclean, "", _("Show the unclean region in red")); + _canvas_debug_show_snapshot.init(_("Show snapshot"), "/options/rendering/debug_show_snapshot", false); + _page_rendering.add_line(true, "", _canvas_debug_show_snapshot, "", _("Show the snapshot region in blue")); + _canvas_debug_show_clean.init(_("Show clean fragmentation"), "/options/rendering/debug_show_clean", false); + _page_rendering.add_line(true, "", _canvas_debug_show_clean, "", _("Show the outlines of the rectangles in the clean region in green")); + _canvas_debug_disable_redraw.init(_("Disable redraw"), "/options/rendering/debug_disable_redraw", false); + _page_rendering.add_line(true, "", _canvas_debug_disable_redraw, "", _("Temporarily disable the idle redraw process completely")); + _canvas_debug_sticky_decoupled.init(_("Sticky decoupled mode"), "/options/rendering/debug_sticky_decoupled", false); + _page_rendering.add_line(true, "", _canvas_debug_sticky_decoupled, "", _("Stay in decoupled mode even after rendering is complete")); + this->AddPage(_page_rendering, _("Rendering"), PREFS_PAGE_RENDERING); } diff --git a/src/ui/dialog/inkscape-preferences.h b/src/ui/dialog/inkscape-preferences.h index de9a3df02e..cc95c1cd1a 100644 --- a/src/ui/dialog/inkscape-preferences.h +++ b/src/ui/dialog/inkscape-preferences.h @@ -337,25 +337,25 @@ protected: UI::Widget::PrefRadioButton _filter_quality_worse; UI::Widget::PrefRadioButton _filter_quality_worst; + UI::Widget::PrefCombo _canvas_update_strategy; + UI::Widget::PrefSpinButton _canvas_render_time_limit; + UI::Widget::PrefCheckButton _canvas_use_new_bisector; + UI::Widget::PrefSpinButton _canvas_new_bisector_size; + UI::Widget::PrefSpinButton _canvas_max_affine_diff; + UI::Widget::PrefSpinButton _canvas_pad; + UI::Widget::PrefSpinButton _canvas_coarsener_min_size; + UI::Widget::PrefSpinButton _canvas_coarsener_glue_size; + UI::Widget::PrefCheckButton _canvas_debug_framecheck; UI::Widget::PrefCheckButton _canvas_debug_logging; - UI::Widget::PrefCheckButton _canvas_debug_overbisection; UI::Widget::PrefCheckButton _canvas_debug_slow_redraw; + UI::Widget::PrefSpinButton _canvas_debug_slow_redraw_time; UI::Widget::PrefCheckButton _canvas_debug_show_redraw; UI::Widget::PrefCheckButton _canvas_debug_show_unclean; UI::Widget::PrefCheckButton _canvas_debug_show_snapshot; UI::Widget::PrefCheckButton _canvas_debug_show_clean; UI::Widget::PrefCheckButton _canvas_debug_disable_redraw; UI::Widget::PrefCheckButton _canvas_debug_sticky_decoupled; - UI::Widget::PrefSpinButton _canvas_debug_overbisection_size; - UI::Widget::PrefSpinButton _canvas_debug_slow_redraw_time; - - UI::Widget::PrefCombo _canvas_update_strategy; - UI::Widget::PrefSpinButton _canvas_render_time_limit; - UI::Widget::PrefSpinButton _canvas_max_affine_diff; - UI::Widget::PrefSpinButton _canvas_pad; - UI::Widget::PrefSpinButton _canvas_coarsener_min_size; - UI::Widget::PrefSpinButton _canvas_coarsener_glue_size; UI::Widget::PrefCheckButton _trans_scale_stroke; UI::Widget::PrefCheckButton _trans_scale_corner; diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index b568d86700..3393a5b649 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -92,13 +92,6 @@ namespace Inkscape { namespace UI { namespace Widget { -struct PaintRectSetup { - Cairo::RefPtr clean_region; - Geom::IntPoint mouse_loc; - int max_pixels; - gint64 start_time; -}; - // Preferences system template @@ -158,24 +151,24 @@ struct Prefs // New parameters Pref update_strategy = Pref ("/options/rendering/update_strategy", 3, 1, 3); Pref render_time_limit = Pref ("/options/rendering/render_time_limit", 1000, 100, 1000000); + Pref use_new_bisector = Pref ("/options/rendering/use_new_bisector", true); + Pref new_bisector_size = Pref ("/options/rendering/new_bisector_size", 400, 1, 10000); Pref max_affine_diff = Pref("/options/rendering/max_affine_diff", 1.8, 0.0, 100.0); Pref pad = Pref ("/options/rendering/pad", 200, 0, 1000); Pref coarsener_min_size = Pref ("/options/rendering/coarsener_min_size", 200, 0, 1000); Pref coarsener_glue_size = Pref ("/options/rendering/coarsener_glue_size", 80, 0, 1000); // Debug switches - Pref debug_framecheck = Pref ("/options/rendering/debug/framecheck"); - Pref debug_logging = Pref ("/options/rendering/debug/logging"); - Pref debug_overbisection = Pref ("/options/rendering/debug/overbisection", true); - Pref debug_overbisection_size = Pref ("/options/rendering/debug/overbisection_size", 400, 1, 10000); - Pref debug_slow_redraw = Pref ("/options/rendering/debug/slow_redraw"); - Pref debug_slow_redraw_time = Pref ("/options/rendering/debug/slow_redraw_time", 50, 0, 1000000); - Pref debug_show_redraw = Pref ("/options/rendering/debug/show_redraw"); - Pref debug_show_unclean = Pref ("/options/rendering/debug/show_unclean"); - Pref debug_show_snapshot = Pref ("/options/rendering/debug/show_snapshot"); - Pref debug_show_clean = Pref ("/options/rendering/debug/show_clean"); - Pref debug_disable_redraw = Pref ("/options/rendering/debug/disable_redraw"); - Pref debug_sticky_decoupled = Pref ("/options/rendering/debug/sticky_decoupled"); + Pref debug_framecheck = Pref ("/options/rendering/debug_framecheck"); + Pref debug_logging = Pref ("/options/rendering/debug_logging"); + Pref debug_slow_redraw = Pref ("/options/rendering/debug_slow_redraw"); + Pref debug_slow_redraw_time = Pref ("/options/rendering/debug_slow_redraw_time", 50, 0, 1000000); + Pref debug_show_redraw = Pref ("/options/rendering/debug_show_redraw"); + Pref debug_show_unclean = Pref ("/options/rendering/debug_show_unclean"); + Pref debug_show_snapshot = Pref ("/options/rendering/debug_show_snapshot"); + Pref debug_show_clean = Pref ("/options/rendering/debug_show_clean"); + Pref debug_disable_redraw = Pref ("/options/rendering/debug_disable_redraw"); + Pref debug_sticky_decoupled = Pref ("/options/rendering/debug_sticky_decoupled"); }; // Conversion functions @@ -227,7 +220,7 @@ public: virtual void mark_clean(const Geom::IntRect &rect) {clean_region->do_union (geom_to_cairo(rect));} // Called on every rectangle redrawn. virtual Cairo::RefPtr get_next_clean_region() {return clean_region;}; // Called once in on_idle to determine what regions to consider clean for the current draw. - virtual bool finished_drawing () {return false;} // Called in on_idle if drawing has finished. Returns true to indicate that drawing should continue, because the next clean region will be different. + virtual bool report_finished () {return false;} // Called in on_idle if drawing has finished. Returns true to indicate that drawing should continue, because the next clean region will be different. virtual void frame () {} // Called by on_draw to notify the end of drawing and the display of the frame. }; @@ -282,7 +275,7 @@ public: } } - bool finished_drawing() override + bool report_finished() override { assert(inprogress); if (!old_clean_region) { @@ -334,7 +327,7 @@ public: return result; } - bool finished_drawing() override + bool report_finished() override { // We have finished work if we have completed a redraw using the depth 0 clean region, which is always the true clean region. bool ret = depth > 0; @@ -394,7 +387,7 @@ public: void add_idle(); bool on_idle(); - bool paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &this_rect); + void paint_rect_internal(Geom::IntRect const &rect); void paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtr &store); // Important global properties of all the stores. If these change, all the stores must be recreated. @@ -441,6 +434,9 @@ public: bool on_lopri_idle(); void update_ctrl_sizes(int size) {q->_canvas_item_root->update_canvas_item_ctrl_sizes(size);} + + std::optional old_bisector(const Geom::IntRect &rect); + std::optional new_bisector(const Geom::IntRect &rect); }; void CanvasPrivate::schedule_bucket_emptier() @@ -523,7 +519,7 @@ Canvas::Canvas() d->prefs.grabsize.action = [=] {_canvas_item_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 = [=] {queue_draw();}; + d->prefs.debug_disable_redraw.action = [=] {d->add_idle();}; d->prefs.debug_sticky_decoupled.action = [=] {d->add_idle();}; d->prefs.update_strategy.action = [=] {d->updater = make_updater(d->prefs.update_strategy, d->updater->clean_region);}; @@ -1379,6 +1375,66 @@ coarsen(const Cairo::RefPtr ®ion, int min_size, int glue_size) 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) +{ + 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) { + return Geom::X; + } + } else { + if (bh > prefs.new_bisector_size) { + return Geom::Y; + } + } + + return {}; +} + bool CanvasPrivate::on_hipri_idle() { @@ -1448,8 +1504,6 @@ CanvasPrivate::on_idle() if (prefs.debug_logging) std::cout << "Full reset" << std::endl; } - // Todo: Consider incrementally and pre-emptively performing this operation across several frames to avoid lag spikes. - // Todo: Screw that. Just do it on the GPU. auto shift_store = [&, this] { // Recreate the store, but keep re-usable content from the old store. auto store_rect = Geom::IntRect::from_xywh( q->_x0, q->_y0, q->get_allocation().get_width(), q->get_allocation().get_height() ); @@ -1574,12 +1628,12 @@ CanvasPrivate::on_idle() q->_need_update = false; } - // Todo: if redrawing is measured to be extremely heavy, we could consider deferring the start of redrawing until the viewing parameters have changed sufficiently much. - // There are only two main obstructions to implementing this feature. First, having to write timing code to measure whether redraw is heavy. Second, and more importantly, - // it can't be implemented without the Canvas being hooked up to a "transforming finished" event, say mouse release. As these are annoying and the benefit is minor, I'm - // not considering implementing this feature. But it would go here. + // If asked to, don't paint anything and instead halt the idle process. + if (prefs.debug_disable_redraw) { + return false; + } - // Get the rectangle of store that is visible. + // Get the subrectangle of store that is visible. Geom::OptIntRect visible_rect; if (!decoupled_mode) { // By a previous assertion, this always lies within the store. @@ -1595,40 +1649,10 @@ CanvasPrivate::on_idle() // The visible rect is the intersection of this with the store visible_rect = bi & _store_rect; - - // Note: We could have used a smaller region containing of consisting of many rectangles, rather than a single bounding box. - // However, while this uses less pixels it also uses more rectangles, so is NOT necessarily an optimisation and could backfire. - // For this reason, and for simplicity, we use the bounding rect. (Note: Slightly invalidated by addition of coarsener below.) } // The visible rectangle must be a subrectangle of store. assert(_store_rect.contains(visible_rect)); - // Get the region to paint, which is the visible rectangle minus the clean region (both subregions of store). - Cairo::RefPtr clean_region = updater->get_next_clean_region(); - Cairo::RefPtr paint_region; - if (visible_rect) { - paint_region = Cairo::Region::create(geom_to_cairo(*visible_rect)); - paint_region->subtract(clean_region); - } else { - paint_region = Cairo::Region::create(); - } - - // Get the list of rectangles to paint, coarsened to avoid fragmentation. - auto paint_rects = coarsen(paint_region, prefs.coarsener_min_size, prefs.coarsener_glue_size); - - // Clip the coarsened rectangles back to the visible rect, in case coarsening made them go outside it. - // Todo: That should be impossible, but somehow it occasionally happens. Or was that in a bad dream? Understand! - for (auto it = paint_rects.begin(); it != paint_rects.end(); ) { - auto opt = *it & *visible_rect; - if (opt) { - *it = *opt; - ++it; - } else { - *it = paint_rects.back(); - paint_rects.pop_back(); - } - } - // Get the mouse position in screen space. Geom::IntPoint mouse_loc; if (auto window = q->get_window()) { @@ -1647,60 +1671,95 @@ CanvasPrivate::on_idle() mouse_loc = geom_act(_store_affine * q->_affine.inverse(), mouse_loc); } - // Todo: Consider further subdividing the region if the mouse lies on the edge of a big rectangle. - // Otherwise, the whole of the big rectangle will be rendered first, at the expense of points in other - // rectangles very near to the mouse. - // Todo: Will be fixed by non-recursive heap-based bisector soon. + // Begin processing redraws. + while (true) { + // Get the clean region for the next redraw as reported by the updater. + auto clean_region = updater->get_next_clean_region(); - // Sort the rectangles to paint by distance from mouse. - std::sort(paint_rects.begin(), paint_rects.end(), [&] (const Geom::IntRect &a, const Geom::IntRect &b) { - return distSq(mouse_loc, a) < distSq(mouse_loc, b); - }); + // Get the region to paint, which is the visible rectangle minus the clean region (both subregions of store). + Cairo::RefPtr paint_region; + if (visible_rect) { + paint_region = Cairo::Region::create(geom_to_cairo(*visible_rect)); + paint_region->subtract(clean_region); + } else { + paint_region = Cairo::Region::create(); + } - // Set up painting info to pass down the stack. - PaintRectSetup setup; - setup.clean_region = clean_region; - setup.mouse_loc = mouse_loc; - if (q->_render_mode != Inkscape::RenderMode::OUTLINE) { - // Can't be too small or large gradient will be rerendered too many times! - setup.max_pixels = 65536 * prefs.tile_multiplier; - } else { - // Paths only. 1M is catched buffer and we need four channels. - setup.max_pixels = 262144; - } - setup.start_time = g_get_monotonic_time(); + // Get the list of rectangles to paint, coarsened to avoid fragmentation. + auto rects = coarsen(paint_region, prefs.coarsener_min_size, prefs.coarsener_glue_size); - // If asked to, don't paint anything and instead halt the idle process. - if (prefs.debug_disable_redraw) { - return false; - } - - // Paint the rectangles. - for (const auto &rect : paint_rects) { - if (rect.hasZeroArea()) { - // Todo: I'm not sure if these can get through or not. If not, remove this check. This is a question about cairo. - continue; + // Ensure that all the rectangles lie within the visible rect (and therefore within the store). + #ifndef NDEBUG + for (auto &rect : rects) { + assert(visible_rect.contains(rect)); } - if (!paint_rect_internal(setup, rect)) { - // Timed out. Temporarily return to GTK main loop, and come back here when next idle. - if (prefs.debug_logging) std::cout << "Timed out: " << g_get_monotonic_time() - setup.start_time << " us" << std::endl; - framecheckobj.subtype = 1; - return true; + #endif + + // 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); + + // Process rectangles until none left or timed out. + auto start_time = g_get_monotonic_time(); + 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(); + + // Cull empty rectangles. + if (rect.width() == 0 || rect.height() == 0) { + continue; + } + + // 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_region->contains_rectangle(geom_to_cairo(rect)) == Cairo::REGION_OVERLAP_IN) { + continue; + } + + // 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. + auto axis = prefs.use_new_bisector ? new_bisector(rect) : old_bisector(rect); + if (axis) { + 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; + } + + // Paint the rectangle. + paint_rect_internal(rect); + + // Check for timeout. + 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: " << g_get_monotonic_time() - start_time << " us" << std::endl; + framecheckobj.subtype = 1; + return true; + } } - } - // Finished redrawing the region requested by the updater. Check if it has any subsequent requests. - if (updater->finished_drawing()) { - // There is another request; continue redrawing. - return true; // Todo: Would be more logical to jump back to the required point, rather than returning. + // Report the redraw as finished. Exit if there's no more redraws to process. + bool keep_going = updater->report_finished(); + if (!keep_going) break; } - // Implement the sticky decoupled mode debug switch. - if (prefs.debug_sticky_decoupled && decoupled_mode) return false; - // Finished drawing. Handle transitions out of decoupled mode, by checking if we need to do a final redraw at the correct affine. if (decoupled_mode) { - if (_store_affine == q->_affine) { + if (prefs.debug_sticky_decoupled) { + // Debug feature: quit idle process, but stay in decoupled mode. + return false; + } else if (_store_affine == q->_affine) { // Content is rendered at the correct affine - exit decoupled mode and quit idle process. if (prefs.debug_logging) std::cout << "Finished drawing - exiting decoupled mode" << std::endl; // Exit decoupled mode. @@ -1726,124 +1785,24 @@ CanvasPrivate::on_idle() * Returns false to bail out in the event of a timeout. * Queues Gtk redraw of widget. */ -bool -CanvasPrivate::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect const &this_rect) -{ - int bw = this_rect.width(); - int bh = this_rect.height(); - - if (bw < 1 || bh < 1) { - // Nothing to draw! - return true; - } - - // Due to coarsening, it's possible for bisected rectangles to lie entirely inside the clean region. These can be discarded, and in fact must be, - // because otherwise we risk rendering only clean rectangles for the whole frame, which would lead to a render stall. - if (setup.clean_region->contains_rectangle(geom_to_cairo(this_rect)) == Cairo::REGION_OVERLAP_IN) { - // Rectangle is already clean. - return true; - } - - // Bisect rectangles that are too big. - if (!prefs.debug_overbisection) { - // Use original bisector. - // Todo: I still don't understand the rationale behind the following strategy. But I'm keeping it default for now. - - /* - * 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. - */ - - if (bw * bh > setup.max_pixels) { - if (bw < bh || bh < 2 * prefs.tile_size) { - int mid = this_rect[Geom::X].middle(); - - auto lo = Geom::IntRect(this_rect.left(), this_rect.top(), mid, this_rect.bottom()); - auto hi = Geom::IntRect(mid, this_rect.top(), this_rect.right(), this_rect.bottom()); - - if (setup.mouse_loc[Geom::X] < mid) { - // Always paint towards the mouse first - return paint_rect_internal(setup, lo) - && paint_rect_internal(setup, hi); - } else { - return paint_rect_internal(setup, hi) - && paint_rect_internal(setup, lo); - } - } else { - int mid = this_rect[Geom::Y].middle(); - - auto lo = Geom::IntRect(this_rect.left(), this_rect.top(), this_rect.right(), mid ); - auto hi = Geom::IntRect(this_rect.left(), mid, this_rect.right(), this_rect.bottom()); - - if (setup.mouse_loc[Geom::Y] < mid) { - // Always paint towards the mouse first - return paint_rect_internal(setup, lo) - && paint_rect_internal(setup, hi); - } else { - return paint_rect_internal(setup, hi) - && paint_rect_internal(setup, lo); - } - } - } - } else { - // Use the new bisector, which just chops in half along the larger dimension. - if (bw > bh) { - if (bw > prefs.debug_overbisection_size) { - int mid = this_rect[Geom::X].middle(); - - auto lo = Geom::IntRect(this_rect.left(), this_rect.top(), mid, this_rect.bottom()); - auto hi = Geom::IntRect(mid, this_rect.top(), this_rect.right(), this_rect.bottom()); - - if (setup.mouse_loc[Geom::X] < mid) { - return paint_rect_internal(setup, lo) - && paint_rect_internal(setup, hi); - } else { - return paint_rect_internal(setup, hi) - && paint_rect_internal(setup, lo); - } - } - } else { - if (bh > prefs.debug_overbisection_size) { - int mid = this_rect[Geom::Y].middle(); - - auto lo = Geom::IntRect(this_rect.left(), this_rect.top(), this_rect.right(), mid ); - auto hi = Geom::IntRect(this_rect.left(), mid, this_rect.right(), this_rect.bottom()); - - if (setup.mouse_loc[Geom::Y] < mid) { - return paint_rect_internal(setup, lo) - && paint_rect_internal(setup, hi); - } else { - return paint_rect_internal(setup, hi) - && paint_rect_internal(setup, lo); - } - } - } - } - +void +CanvasPrivate::paint_rect_internal(Geom::IntRect const &rect) +{ // Paint the rectangle. q->_drawing->setRenderMode(q->_render_mode); q->_drawing->setColorMode(q->_color_mode); - paint_single_buffer(this_rect, _backing_store); + paint_single_buffer(rect, _backing_store); // Introduce an artificial delay for each rectangle. if (prefs.debug_slow_redraw) g_usleep(prefs.debug_slow_redraw_time); // Mark the rectangle as clean. - updater->mark_clean(this_rect); + updater->mark_clean(rect); // Mark the screen dirty. if (!decoupled_mode) { // Get rectangle needing repaint - auto repaint_rect = this_rect - Geom::IntPoint(q->_x0, q->_y0); + auto repaint_rect = rect - Geom::IntPoint(q->_x0, q->_y0); // Assert that a repaint actually occurs (guaranteed because we are only asked to paint fully on-screen rectangles) auto screen_rect = Geom::IntRect(0, 0, q->get_allocation().get_width(), q->get_allocation().get_height()); @@ -1854,7 +1813,7 @@ CanvasPrivate::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect co pending_bucket_emptier = true; // On the next call to on_draw, schedules the bucket emptier. } else { // Get rectangle needing repaint (transform into screen space, take bounding box, round outwards) - auto pl = Geom::Parallelogram(this_rect); + auto pl = Geom::Parallelogram(rect); pl *= q->_affine * _store_affine.inverse(); pl *= Geom::Translate(-q->_x0, -q->_y0); auto b = pl.bounds(); @@ -1868,16 +1827,6 @@ CanvasPrivate::paint_rect_internal(PaintRectSetup const &setup, Geom::IntRect co pending_bucket_emptier = true; } } - - // Exit if timed out. - auto now = g_get_monotonic_time(); - auto elapsed = now - setup.start_time; - if (elapsed > prefs.render_time_limit) { - return false; - } - - // Continue rendering. - return true; } /* -- GitLab From 38c31f076387f2a95641c9c78d47a405cbb29ae4 Mon Sep 17 00:00:00 2001 From: PBS Date: Sun, 23 Jan 2022 18:38:40 +0900 Subject: [PATCH 24/35] Lazy activation for multiscale updater, plus a tweak * Lazy activation means zero memory/cpu cost unless actually needed. (The full redraw updater already had this feature.) * The tweak - blocked = {Cairo::Region::create()}; + blocked[scale] = clean_region->copy(); also has the subjective effect of making the artifacts less annoying, seemingly by making them less full of holes. * Many improvements to comments; variable names now more logical. --- src/ui/widget/canvas.cpp | 125 ++++++++++++++++++++++++--------------- 1 file changed, 77 insertions(+), 48 deletions(-) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 3393a5b649..0210ff4a0b 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -212,16 +212,16 @@ public: // The subgregion of the store with up-to-date content. Cairo::RefPtr clean_region; - Updater(Cairo::RefPtr clean_region) : clean_region(clean_region) {} + Updater(Cairo::RefPtr clean_region) : clean_region(std::move(clean_region)) {} virtual void reset() {clean_region = Cairo::Region::create();} // Reset to clean region to empty. - virtual void intersect (const Geom::IntRect &rect) {clean_region->intersect(geom_to_cairo(rect));} // Called when the store changes position to clip everthing to the new store rect. + virtual void intersect (const Geom::IntRect &rect) {clean_region->intersect(geom_to_cairo(rect));} // Called when the store changes position; clip everthing to the new store rect. virtual void mark_dirty(const Geom::IntRect &rect) {clean_region->subtract (geom_to_cairo(rect));} // Called on every invalidate event. virtual void mark_clean(const Geom::IntRect &rect) {clean_region->do_union (geom_to_cairo(rect));} // Called on every rectangle redrawn. - virtual Cairo::RefPtr get_next_clean_region() {return clean_region;}; // Called once in on_idle to determine what regions to consider clean for the current draw. - virtual bool report_finished () {return false;} // Called in on_idle if drawing has finished. Returns true to indicate that drawing should continue, because the next clean region will be different. - virtual void frame () {} // Called by on_draw to notify the end of drawing and the display of the frame. + virtual Cairo::RefPtr get_next_clean_region() {return clean_region;}; // Called by on_idle to determine what regions to consider clean for the current redraw. + virtual bool report_finished () {return false;} // Called in on_idle if the redraw has finished. Returns true to indicate that further redraws are required with a different clean region. + virtual void frame () {} // Called by on_draw to notify the updater of the display of the frame. }; // Responsive updater: As soon as a region is invalidated, redraw it. @@ -231,13 +231,13 @@ using ResponsiveUpdater = Updater; class FullredrawUpdater : public Updater { // Whether we are currently in the middle of a redraw. - bool inprogress; + bool inprogress = false; - // If more damage events arrive while a redraw is in progress, save the clean region to here so we can continue the redraw using it. + // Contains a copy of the old clean region if damage events occurred during the current redraw, otherwise null. Cairo::RefPtr old_clean_region; public: - FullredrawUpdater(Cairo::RefPtr clean_region) : Updater(clean_region), inprogress(false) {} + FullredrawUpdater(Cairo::RefPtr clean_region) : Updater(std::move(clean_region)) {} void reset() override { @@ -254,7 +254,7 @@ public: void mark_dirty(const Geom::IntRect &rect) override { - if (inprogress && !old_clean_region) old_clean_region = clean_region->copy(); // CoW + if (inprogress && !old_clean_region) old_clean_region = clean_region->copy(); Updater::mark_dirty(rect); } @@ -266,9 +266,8 @@ public: Cairo::RefPtr get_next_clean_region() override { - // Return the old clean region if it was saved, otherwise return the current one and set a flag indicating redraw in progress. + inprogress = true; if (!old_clean_region) { - inprogress = true; return clean_region; } else { return old_clean_region; @@ -279,11 +278,11 @@ public: { assert(inprogress); if (!old_clean_region) { - // Redrawn using the actual clean region --> finished. + // Completed redraw without being damaged => finished. inprogress = false; return false; } else { - // Redrawn using the old clean region; new clean region has been damaged --> draw again using the new clean region. + // Completed redraw but damage events arrived => ask for another redraw, using the up-to-date clean region. old_clean_region.clear(); return true; } @@ -293,73 +292,104 @@ public: // Multiscale updater: Updates tiles near the mouse faster. Gives the best of both. class MultiscaleUpdater : public Updater { - int counter, depth, counter2; - std::vector> blocked; + // Whether we are currently in the middle of a redraw. + bool inprogress = false; + + // Whether damage events occurred during the current redraw. + bool activated = false; + + int counter; // A steadily incrementing counter from which the current scale is derived. + int scale; // The current scale to process updates at. + int elapsed; // How much time has been spent at the current scale. + std::vector> blocked; // The region blocked from being updated at each scale. public: - MultiscaleUpdater(Cairo::RefPtr clean_region) : Updater(clean_region), counter(0), depth(0), counter2(0), blocked({Cairo::Region::create()}) {} + MultiscaleUpdater(Cairo::RefPtr clean_region) : Updater(std::move(clean_region)) {} void reset() override { Updater::reset(); - counter = depth = counter2 = 0; - blocked = {Cairo::Region::create()}; + inprogress = activated = false; } void intersect(const Geom::IntRect &rect) override { Updater::intersect(rect); - for (auto ® : blocked) { - reg->intersect(geom_to_cairo(rect)); + if (activated) { + for (auto ® : blocked) { + reg->intersect(geom_to_cairo(rect)); + } } } + void mark_dirty(const Geom::IntRect &rect) override + { + Updater::mark_dirty(rect); + if (inprogress && !activated) { + counter = scale = elapsed = 0; + blocked = {Cairo::Region::create()}; + activated = true; + } + } + void mark_clean(const Geom::IntRect &rect) override { Updater::mark_clean(rect); - blocked[depth]->do_union(geom_to_cairo(rect)); + if (activated) blocked[scale]->do_union(geom_to_cairo(rect)); } Cairo::RefPtr get_next_clean_region() override { - auto result = clean_region->copy(); - result->do_union(blocked[depth]); - return result; + inprogress = true; + if (!activated) { + return clean_region; + } else { + auto result = clean_region->copy(); + result->do_union(blocked[scale]); + return result; + } } bool report_finished() override { - // We have finished work if we have completed a redraw using the depth 0 clean region, which is always the true clean region. - bool ret = depth > 0; - // Perform a reset of the updating sequence in both cases. - counter = depth = counter2 = 0; - blocked = {Cairo::Region::create()}; - return ret; + assert(inprogress); + if (!activated) { + // Completed redraw without damage => finished. + inprogress = false; + return false; + } else { + // Completed redraw but damage events arrived => begin updating any remaining damaged regions. + activated = false; + blocked.clear(); + return true; + } } void frame() override { - // Stay at the current depth for 2^depth frames. - counter2++; - if (counter2 < (1 << depth)) return; - counter2 = 0; + if (!activated) return; + + // Stay at the current scale for 2^scale frames. + elapsed++; + if (elapsed < (1 << scale)) return; + elapsed = 0; - // Adjust the counter, which causes depth to hop around the values 0, 1, 2... spending half as much time at each subsequent depth. + // Adjust the counter, which causes scale to hop around the values 0, 1, 2... spending half as much time at each subsequent scale. counter++; - depth = 0; + scale = 0; for (int tmp = counter; tmp % 2 == 1; tmp /= 2) { - depth++; + scale++; } - // Ensure enough blocked zones exist. - if (depth == blocked.size()) { + // Ensure sufficiently many blocked zones exist. + if (scale == blocked.size()) { blocked.emplace_back(); } - // Recreate the current blocked zone as the union of those previous to it. - blocked[depth] = Cairo::Region::create(); - for (int i = 0; i < depth; i++) { - blocked[depth]->do_union(blocked[i]); + // Recreate the current blocked zone as the union of the clean region and lower-scale blocked zones. + blocked[scale] = clean_region->copy(); + for (int i = 0; i < scale; i++) { + blocked[scale]->do_union(blocked[i]); } } }; @@ -790,7 +820,7 @@ Cairo::RefPtr Canvas::get_backing_store() const void Canvas::forced_redraws_start(int count, bool reset) { - // Todo: Likely to be removed, depending on feedback. + // Todo: not used; remove when ready. } /** @@ -1495,7 +1525,7 @@ CanvasPrivate::on_idle() if (prefs.debug_show_unclean) q->queue_draw(); }; - // Determine the rendering parameters have changed, and reset if so. + // Determine whether the rendering parameters have changed, and reset if so. if (!_backing_store || _device_scale != q->get_scale_factor() || _store_solid_background != solid_background) { _device_scale = q->get_scale_factor(); _store_solid_background = solid_background; @@ -1584,8 +1614,7 @@ CanvasPrivate::on_idle() // If the store has gone completely off-screen, recreate it. recreate_store(); if (prefs.debug_logging) std::cout << "Recreated store" << std::endl; - } else if (!_store_rect.contains(visible)) - { + } else if (!_store_rect.contains(visible)) { // If the store has gone partially off-screen, shift it. shift_store(); if (prefs.debug_logging) std::cout << "Shifted store" << std::endl; @@ -1662,7 +1691,7 @@ CanvasPrivate::on_idle() window->get_device_position(Gdk::Display::get_default()->get_default_seat()->get_pointer(), x, y, mask); mouse_loc = Geom::IntPoint(x, y); } else { - mouse_loc = Geom::IntPoint(0, 0); // Doesn't particularly matter, just as long as it's initialised. + mouse_loc = Geom::IntPoint(); // Doesn't particularly matter, just as long as it's initialised. } // Map the mouse to canvas space. -- GitLab From 44c36fe88f6ee37256aaf8f04ac66b6dfd2d2be1 Mon Sep 17 00:00:00 2001 From: PBS Date: Mon, 24 Jan 2022 22:43:55 +0900 Subject: [PATCH 25/35] Documentation updates, organisation improvements. --- src/desktop.cpp | 3 +- src/ui/widget/canvas.cpp | 201 ++++++++++++++++++--------------------- src/ui/widget/canvas.h | 18 ++-- 3 files changed, 100 insertions(+), 122 deletions(-) diff --git a/src/desktop.cpp b/src/desktop.cpp index 49feb09e4a..e4636ad9a5 100644 --- a/src/desktop.cpp +++ b/src/desktop.cpp @@ -632,7 +632,7 @@ void SPDesktop::set_display_area( Geom::Rect const &r, double border, bool log) { // Create a rectangle the size of the window aligned with origin. - Geom::Rect w( Geom::Point(), canvas->get_area_world().dimensions() ); + Geom::Rect w( Geom::Point(), canvas->get_dimensions() ); // Shrink window to account for border padding. w.expandBy( -border ); @@ -902,7 +902,6 @@ SPDesktop::rotate_relative_center_point (Geom::Point const &c, double rotate) set_display_area(c, viewbox.midpoint()); } - /** * Set new flip direction, keeping the point 'c' fixed in the desktop window. * diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 0210ff4a0b..7e4627dbf9 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -44,40 +44,28 @@ * * * redraw_all() Redraws the entire canvas by calling redraw_area() with the canvas area. * - * * redraw_area() Redraws the indicated area. Use when there is a change that doesn't effect + * * redraw_area() Redraws the indicated area. Use when there is a change that doesn't affect * a CanvasItem's geometry or size. * - * * request_update() Redraws after recalculating bounds for changed CanvasItem's. Use if a + * * request_update() Redraws after recalculating bounds for changed CanvasItems. Use if a * CanvasItem's geometry or size has changed. * - * * redraw_now() Redraw immediately, skipping the "idle" stage. - * * The first three functions add a request to the Gtk's "idle" list via * * * add_idle() Which causes Gtk to call when resources are available: * - * * on_idle() Which calls: - * - * * do_update() Which makes a few checks and then calls: - * - * * paint() Which calls for each area of the canvas that has been marked unclean: + * * on_idle() Which sets up the backing stores, divides the area of the canvas that has been marked + * unclean into rectangles that are small enough to render quickly, and renders them outwards + * from the mouse with a call to: * - * * paint_rect() Which determines the maximum area to draw at once and where the cursor is, then calls: + * * paint_rect_internal() Which paints the rectangle using paint_single_buffer(). It renders onto a Cairo + * surface "backing_store". After a piece is rendered there is a call to: * - * * paint_rect_internal() Which recursively divides the area into smaller pieces until a piece is small - * enough to render. It renders the pieces closest to the cursor first. The pieces - * are rendered onto a Cairo surface "backing_store". After a piece is rendered - * there is a call to: - * - * * queue_draw_area() A Gtk function for drawing into a widget which when the time is right calls: + * * queue_draw_area() A Gtk function for marking areas of the window as needing a repaint, which when + * the time is right calls: * * * on_draw() Which blits the Cairo surface to the screen. * - * One thing to note is that on_draw() must be called twice to render anything to the screen, as the - * first time through it sets up the backing store which must then be drawn to. The second call then - * blits the backing store to the screen. It might be better to setup the backing store on a call - * to on_allocate() but it is what works now. - * * The other responsibility of the canvas is to determine where to send GUI events. It does this * by determining which CanvasItem is "picked" and then forwards the events to that item. Not all * items can be picked. As a last resort, the "CatchAll" CanvasItem will be picked as it is the @@ -237,6 +225,7 @@ class FullredrawUpdater : public Updater Cairo::RefPtr old_clean_region; public: + FullredrawUpdater(Cairo::RefPtr clean_region) : Updater(std::move(clean_region)) {} void reset() override @@ -304,6 +293,7 @@ class MultiscaleUpdater : public Updater std::vector> blocked; // The region blocked from being updated at each scale. public: + MultiscaleUpdater(Cairo::RefPtr clean_region) : Updater(std::move(clean_region)) {} void reset() override @@ -398,10 +388,10 @@ std::unique_ptr make_updater(int type, Cairo::RefPtr clean_region = Cairo::Region::create()) { switch (type) { - case 1: return std::make_unique(clean_region); - case 2: return std::make_unique(clean_region); + case 1: return std::make_unique(std::move(clean_region)); + case 2: return std::make_unique(std::move(clean_region)); default: - case 3: return std::make_unique(clean_region); + case 3: return std::make_unique(std::move(clean_region)); } } @@ -418,30 +408,43 @@ public: void add_idle(); bool on_idle(); void paint_rect_internal(Geom::IntRect const &rect); - void paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtr &store); + void paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtr const &store); + + std::optional old_bisector(const Geom::IntRect &rect); + std::optional new_bisector(const Geom::IntRect &rect); // Important global properties of all the stores. If these change, all the stores must be recreated. int _device_scale = 1; bool _store_solid_background; + bool _have_outline_store; - Geom::IntRect _store_rect; ///< Rectangle of the store in world space. + // The backing store. + Geom::IntRect _store_rect; Geom::Affine _store_affine; - Cairo::RefPtr _backing_store; ///< Canvas content. + Cairo::RefPtr _backing_store; + Cairo::RefPtr _outline_store; - std::unique_ptr updater; // Holds the clean region, the subregion of the store with up-to-date content, and decides in what order the unclean regions should be redrawn. + // The updater. Holds the clean region, the subregion of the store with up-to-date content, and some additional state determining in what order the unclean regions should be redrawn. + std::unique_ptr updater; + // The snapshot store. Used to mask redraw delay on zoom/rotate. Geom::IntRect _snapshot_rect; Geom::Affine _snapshot_affine; Cairo::RefPtr _snapshot_store; Cairo::RefPtr _snapshot_clean_region; + Geom::Affine geom_affine; // The affine the geometry was last imbued with. bool decoupled_mode = false; - bool solid_background; + bool solid_background; // Whether the last background set is solid. + // Idle system callbacks. The high priority idle ensures at least one idle cycle between add_idle and on_draw. sigc::connection hipri_idle; sigc::connection lopri_idle; + bool on_hipri_idle(); + bool on_lopri_idle(); + // The event bucket. Events that arrive during a frame are put in the bucket, and the bucket is emptied immediately after the next on_draw, leaving the whole rest of the frame to proces the events. struct GdkEventFreer {void operator()(GdkEvent *ev) const {gdk_event_free(ev);}}; std::vector> bucket; sigc::connection bucket_emptier; @@ -454,19 +457,12 @@ public: GdkEvent *ignore = nullptr; - Geom::Affine geom_affine; - + // Useful overload of GtkWidget function. void queue_draw_area(Geom::IntRect &rect); + // Preferences system Prefs prefs; - - bool on_hipri_idle(); - bool on_lopri_idle(); - void update_ctrl_sizes(int size) {q->_canvas_item_root->update_canvas_item_ctrl_sizes(size);} - - std::optional old_bisector(const Geom::IntRect &rect); - std::optional new_bisector(const Geom::IntRect &rect); }; void CanvasPrivate::schedule_bucket_emptier() @@ -567,8 +563,6 @@ Canvas::Canvas() _canvas_item_root = new Inkscape::CanvasItemGroup(nullptr); _canvas_item_root->set_name("CanvasItemGroup:Root"); _canvas_item_root->set_canvas(this); - - if (d->prefs.debug_show_redraw) srand(g_get_monotonic_time()); // Initialise seed for random colours. } Canvas::~Canvas() @@ -586,15 +580,19 @@ Canvas::~Canvas() delete _canvas_item_root; } +Geom::IntPoint Canvas::get_dimensions() const +{ + Gtk::Allocation allocation = get_allocation(); + return {allocation.get_width(), allocation.get_height()}; +} + /** * Is world point inside canvas area? */ bool Canvas::world_point_inside_canvas(Geom::Point const &world) const { - Gtk::Allocation allocation = get_allocation(); - return ( _x0 <= world.x() && world.x() < _x0 + allocation.get_width() && - _y0 <= world.y() && world.y() < _y0 + allocation.get_height() ); + return get_area_world().contains(world.floor()); } /** @@ -603,7 +601,7 @@ Canvas::world_point_inside_canvas(Geom::Point const &world) const Geom::Point Canvas::canvas_to_world(Geom::Point const &point) const { - return Geom::Point(point[Geom::X]+ _x0, point[Geom::Y] + _y0); + return point + _pos; } /** @@ -612,8 +610,7 @@ Canvas::canvas_to_world(Geom::Point const &point) const Geom::IntRect Canvas::get_area_world() const { - Gtk::Allocation allocation = get_allocation(); - return Geom::IntRect::from_xywh(_x0, _y0, allocation.get_width(), allocation.get_height()); + return Geom::IntRect(_pos, _pos + get_dimensions()); } /** @@ -660,22 +657,21 @@ Canvas::redraw_area(int x0, int y0, int x1, int y1) return; } - if (x0 >= x1 || y0 >= y1) { - return; - } - // Clamp area to Cairo's technically supported max size (-2^30..+2^30-1). // This ensures that the rectangle dimensions don't overflow and wrap around. - - constexpr int min_coord = std::numeric_limits::min() / 2; - constexpr int max_coord = std::numeric_limits::max() / 2; + constexpr int min_coord = -(1 << 30); + constexpr int max_coord = (1 << 30) - 1; x0 = std::clamp(x0, min_coord, max_coord); y0 = std::clamp(y0, min_coord, max_coord); x1 = std::clamp(x1, min_coord, max_coord); y1 = std::clamp(y1, min_coord, max_coord); - auto rect = Geom::IntRect::from_xywh(x0, y0, x1-x0, y1-y0); + if (x0 >= x1 || y0 >= y1) { + return; + } + + auto rect = Geom::IntRect::from_xywh(x0, y0, x1 - x0, y1 - y0); d->updater->mark_dirty(rect); d->add_idle(); if (d->prefs.debug_show_unclean) queue_draw(); @@ -686,20 +682,19 @@ Canvas::redraw_area(Geom::Coord x0, Geom::Coord y0, Geom::Coord x1, Geom::Coord { // Handle overflow during conversion gracefully. // Round outward to make sure integral coordinates cover the entire area. - - constexpr Geom::Coord min_int = static_cast(std::numeric_limits::min()); - constexpr Geom::Coord max_int = static_cast(std::numeric_limits::max()); + constexpr Geom::Coord min_int = std::numeric_limits::min(); + constexpr Geom::Coord max_int = std::numeric_limits::max(); redraw_area( - static_cast(std::floor(std::clamp(x0, min_int, max_int))), - static_cast(std::floor(std::clamp(y0, min_int, max_int))), - static_cast(std::ceil (std::clamp(x1, min_int, max_int))), - static_cast(std::ceil (std::clamp(y1, min_int, max_int))) + (int)std::floor(std::clamp(x0, min_int, max_int)), + (int)std::floor(std::clamp(y0, min_int, max_int)), + (int)std::ceil (std::clamp(x1, min_int, max_int)), + (int)std::ceil (std::clamp(y1, min_int, max_int)) ); } void -Canvas::redraw_area(Geom::Rect& area) +Canvas::redraw_area(Geom::Rect &area) { redraw_area(area.left(), area.top(), area.right(), area.bottom()); } @@ -724,15 +719,13 @@ Canvas::request_update() void Canvas::scroll_to(Geom::Point const &c) { - int x = (int) std::round(c[Geom::X]); - int y = (int) std::round(c[Geom::Y]); + auto newpos = c.round(); - if (x == _x0 && y == _y0) { + if (newpos == _pos) { return; } - _x0 = x; - _y0 = y; + _pos = newpos; d->add_idle(); queue_draw(); @@ -770,6 +763,14 @@ Canvas::set_background_checkerboard(guint32 rgba, bool use_alpha) redraw_all(); } +void Canvas::set_drawing_disabled(bool disable) +{ + _drawing_disabled = disable; + if (!disable) { + d->add_idle(); + } +} + void Canvas::set_render_mode(Inkscape::RenderMode mode) { @@ -1138,18 +1139,12 @@ void Canvas::on_realize() /* * The on_draw() function is called whenever Gtk wants to update the window. This function: * - * 1. Sets up the backing and outline stores (images). These stores are drawn to elsewhere during idles. - * The backing store is always uses, rendering in which ever "render mode" the user has selected. - * The outline store is only used when the "split mode" is set to 'split' or 'x-ray'. - * (Changing either the render mode or split mode results in a complete redrawing the store(s).) - * - * 2. Calls shift_content() if the drawing area has changed. + * 1. Ensures that if the idle process was started, at least one cycle has run. * - * 3. Blits the store(s) onto the canvas, clipping the outline store as required. + * 2. Blits the store(s) onto the canvas, clipping the outline store as required. + * (Or composites them with the transformed snapshot store in decoupled mode.) * - * 4. Draws the "controller" in the 'split' split mode. - * - * 5. Calls add_idle() to update the drawing if necessary. + * 3. Draws the "controller" in the 'split' split mode. */ bool Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) @@ -1185,7 +1180,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) if (d->prefs.debug_framecheck) f = FrameCheck::Event("draw"); cr->save(); cr->set_operator(d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); - cr->set_source(d->_backing_store, d->_store_rect.left() - _x0, d->_store_rect.top() - _y0); + cr->set_source(d->_backing_store, d->_store_rect.left() - _pos.x(), d->_store_rect.top() - _pos.y()); cr->paint(); cr->restore(); } else { @@ -1198,7 +1193,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->save(); cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD); cr->rectangle(0, 0, get_allocation().get_width(), get_allocation().get_height()); - cr->translate(-_x0, -_y0); + cr->translate(-_pos.x(), -_pos.y()); cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); region_to_path(cr, d->updater->clean_region); cr->transform(geom_to_cairo(d->_store_affine * d->_snapshot_affine.inverse())); @@ -1216,7 +1211,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->save(); cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD); cr->rectangle(0, 0, get_allocation().get_width(), get_allocation().get_height()); - cr->translate(-_x0, -_y0); + cr->translate(-_pos.x(), -_pos.y()); cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); region_to_path(cr, d->updater->clean_region); cr->clip(); @@ -1237,7 +1232,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) // Draw transformed store, clipped to clean region. if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 0); cr->save(); - cr->translate(-_x0, -_y0); + cr->translate(-_pos.x(), -_pos.y()); cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); region_to_path(cr, d->updater->clean_region); cr->clip(); @@ -1254,7 +1249,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) auto reg = Cairo::Region::create(geom_to_cairo(d->_store_rect)); reg->subtract(d->updater->clean_region); cr->save(); - cr->translate(-_x0, -_y0); + cr->translate(-_pos.x(), -_pos.y()); if (d->decoupled_mode) { cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); } @@ -1268,7 +1263,7 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) if (d->prefs.debug_show_clean) { if (d->prefs.debug_framecheck) f = FrameCheck::Event("paint_clean"); cr->save(); - cr->translate(-_x0, -_y0); + cr->translate(-_pos.x(), -_pos.y()); if (d->decoupled_mode) { cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); } @@ -1503,7 +1498,7 @@ CanvasPrivate::on_idle() const Geom::IntPoint pad(prefs.pad, prefs.pad); auto recreate_store = [&, this] { // Recreate the store at the current affine so that it covers the visible region. - _store_rect = Geom::IntRect::from_xywh( q->_x0, q->_y0, q->get_allocation().get_width(), q->get_allocation().get_height() ); + _store_rect = q->get_area_world(); _store_rect.expandBy(pad); _store_affine = q->_affine; int desired_width = _store_rect.width() * _device_scale; @@ -1536,7 +1531,7 @@ CanvasPrivate::on_idle() auto shift_store = [&, this] { // Recreate the store, but keep re-usable content from the old store. - auto store_rect = Geom::IntRect::from_xywh( q->_x0, q->_y0, q->get_allocation().get_width(), q->get_allocation().get_height() ); + auto store_rect = q->get_area_world(); store_rect.expandBy(pad); // Todo: Stop cairo unnecessarily pre-filling the store with transparency below if solid_background is true. auto backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, store_rect.width() * _device_scale, store_rect.height() * _device_scale); @@ -1609,7 +1604,7 @@ CanvasPrivate::on_idle() // or blits are performed, and the drawing operations done on the screen are the same. Hence this feature comes at zero cost. } else { // Get visible rectangle in canvas coordinates. - const auto visible = Geom::IntRect::from_xywh( q->_x0, q->_y0, q->get_allocation().get_width(), q->get_allocation().get_height() ); + auto const visible = q->get_area_world(); if (!_store_rect.intersects(visible)) { // If the store has gone completely off-screen, recreate it. recreate_store(); @@ -1666,10 +1661,10 @@ CanvasPrivate::on_idle() Geom::OptIntRect visible_rect; if (!decoupled_mode) { // By a previous assertion, this always lies within the store. - visible_rect = Geom::IntRect::from_xywh( q->_x0, q->_y0, q->get_allocation().get_width(), q->get_allocation().get_height() ); + visible_rect = q->get_area_world(); } else { // Get the window rectangle transformed into canvas space. - auto pl = Geom::Parallelogram(Geom::IntRect::from_xywh( q->_x0, q->_y0, q->get_allocation().get_width(), q->get_allocation().get_height() )); + auto pl = Geom::Parallelogram(q->get_area_world()); pl *= _store_affine * q->_affine.inverse(); // Get its bounding box, rounded outwards. @@ -1695,7 +1690,7 @@ CanvasPrivate::on_idle() } // Map the mouse to canvas space. - mouse_loc += Geom::IntPoint(q->_x0, q->_y0); + mouse_loc += q->_pos; if (decoupled_mode) { mouse_loc = geom_act(_store_affine * q->_affine.inverse(), mouse_loc); } @@ -1810,10 +1805,6 @@ CanvasPrivate::on_idle() } } -/* - * Returns false to bail out in the event of a timeout. - * Queues Gtk redraw of widget. - */ void CanvasPrivate::paint_rect_internal(Geom::IntRect const &rect) { @@ -1831,7 +1822,7 @@ CanvasPrivate::paint_rect_internal(Geom::IntRect const &rect) // Mark the screen dirty. if (!decoupled_mode) { // Get rectangle needing repaint - auto repaint_rect = rect - Geom::IntPoint(q->_x0, q->_y0); + auto repaint_rect = rect - q->_pos; // Assert that a repaint actually occurs (guaranteed because we are only asked to paint fully on-screen rectangles) auto screen_rect = Geom::IntRect(0, 0, q->get_allocation().get_width(), q->get_allocation().get_height()); @@ -1844,7 +1835,7 @@ CanvasPrivate::paint_rect_internal(Geom::IntRect const &rect) // Get rectangle needing repaint (transform into screen space, take bounding box, round outwards) auto pl = Geom::Parallelogram(rect); pl *= q->_affine * _store_affine.inverse(); - pl *= Geom::Translate(-q->_x0, -q->_y0); + pl *= Geom::Translate(-q->_pos); auto b = pl.bounds(); auto repaint_rect = Geom::IntRect(b.min().floor(), b.max().ceil()); @@ -1858,14 +1849,8 @@ CanvasPrivate::paint_rect_internal(Geom::IntRect const &rect) } } -/* - * Paint a single buffer. - * paint_rect: buffer rectangle. - * canvas_rect: canvas rectangle. - * store: Cairo surface to draw on. - */ void -CanvasPrivate::paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtr &store) +CanvasPrivate::paint_single_buffer(Geom::IntRect const &paint_rect, const Cairo::RefPtr &store) { // Make sure the following code does not go outside of store's data assert(store); @@ -2125,9 +2110,7 @@ Canvas::pick_current_item(GdkEvent *event) } // Convert to world coordinates. - x += _x0; - y += _y0; - Geom::Point p(x, y); + auto p = Geom::Point(x, y) + _pos; _current_canvas_item_new = _canvas_item_root->pick_item(p); // if (_current_canvas_item_new) { @@ -2238,16 +2221,16 @@ Canvas::emit_event(GdkEvent *event) switch (event_copy->type) { case GDK_ENTER_NOTIFY: case GDK_LEAVE_NOTIFY: - event_copy->crossing.x += _x0; - event_copy->crossing.y += _y0; + event_copy->crossing.x += _pos.x(); + event_copy->crossing.y += _pos.y(); break; case GDK_MOTION_NOTIFY: case GDK_BUTTON_PRESS: case GDK_2BUTTON_PRESS: case GDK_3BUTTON_PRESS: case GDK_BUTTON_RELEASE: - event_copy->motion.x += _x0; - event_copy->motion.y += _y0; + event_copy->motion.x += _pos.x(); + event_copy->motion.y += _pos.y(); break; default: break; diff --git a/src/ui/widget/canvas.h b/src/ui/widget/canvas.h index 3f6864bcde..e84e1703e9 100644 --- a/src/ui/widget/canvas.h +++ b/src/ui/widget/canvas.h @@ -33,7 +33,6 @@ namespace UI { namespace Widget { class CanvasPrivate; -struct PaintRectSetup; /** * A Gtk::DrawingArea widget for Inkscape's canvas. @@ -52,6 +51,7 @@ public: SPDesktop *get_desktop() const { return _desktop; } // Geometry + Geom::IntPoint get_dimensions() const; bool world_point_inside_canvas(Geom::Point const &world) const; // desktop-events.cpp Geom::Point canvas_to_world(Geom::Point const &window) const; Geom::IntRect get_area_world() const; @@ -61,16 +61,16 @@ public: // Drawing void set_drawing(Inkscape::Drawing *drawing) { _drawing = drawing; } - void redraw_all(); // Draw entire surface during idle. - void redraw_area(Geom::Rect& area); // Draw specified area during idle. - void request_update(); // Draw after updating canvas items. + void redraw_all(); // Mark everything as having changed. + void redraw_area(Geom::Rect& area); // Mark a rectangle of world space as having changed. + void request_update(); // Mark geometry as needing recalculation. void scroll_to(Geom::Point const &c); void set_background_color(guint32 rgba); void set_background_checkerboard(guint32 rgba = 0xC4C4C4FF, bool use_alpha = false); - void set_drawing_disabled(bool disable) { _drawing_disabled = disable; } // Disable during path ops, etc. - bool is_dragging() const { return _is_dragging; } // selection-chemistry.cpp + void set_drawing_disabled(bool disable); // Disable during path ops, etc. + bool is_dragging() const { return _is_dragging; } // selection-chemistry.cpp // Rendering modes void set_render_mode(Inkscape::RenderMode mode); @@ -148,16 +148,13 @@ private: bool pick_current_item(GdkEvent *event); bool emit_event(GdkEvent *event); - // ==== Signal callbacks ==== - sigc::connection _idle_connection; - // ====== Data members ======= // Structure SPDesktop *_desktop = nullptr; // Geometry - int _x0 = 0, _y0 = 0; ///< Coordinates of top-left pixel of canvas view within canvas. + Geom::IntPoint _pos; ///< Coordinates of top-left pixel of canvas view within canvas. Geom::Affine _affine; ///< The affine that we have been requested to draw at. // Event handling/item picking @@ -201,7 +198,6 @@ private: // Changes to documents should not be triggering changes to closed windows. This fix is a hack.) bool _in_destruction = false; - // ======= CAIRO ======= ... Keep in one place Cairo::RefPtr _background; ///< The background of the widget. // Opaque pointer to implementation -- GitLab From 370e335bb50988232010cd1f8bb084e614a80348 Mon Sep 17 00:00:00 2001 From: PBS Date: Mon, 24 Jan 2022 23:52:09 +0900 Subject: [PATCH 26/35] Tweak coarsener Add another heuristic to the coarsener to make it less aggressive on very thin L-shaped regions. Previously, it would make these into one big rectangle (though no bigger than the min_size parameter). Now, we should hopefully output two long and thin rectangles instead. This was particularly noticeable on scrolling diagonally, where it would ocassionally result in a flicker in 'Show Redraws' mode as an unnecessarily big rectangle got repainted. --- src/ui/dialog/inkscape-preferences.cpp | 8 +-- src/ui/dialog/inkscape-preferences.h | 1 + src/ui/widget/canvas.cpp | 70 +++++++++++++++++++------- 3 files changed, 58 insertions(+), 21 deletions(-) diff --git a/src/ui/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp index 9f28b6aa30..16ce5aeb77 100644 --- a/src/ui/dialog/inkscape-preferences.cpp +++ b/src/ui/dialog/inkscape-preferences.cpp @@ -2709,16 +2709,18 @@ void InkscapePreferences::initPageRendering() _page_rendering.add_line(true, _("Render time limit"), _canvas_render_time_limit, C_("microsecond abbreviation", "μs"), _("The maximum time allowed for a rendering time slice"), false); _canvas_use_new_bisector.init(_("Use new bisector"), "/options/rendering/use_new_bisector", true); _page_rendering.add_line(true, "", _canvas_use_new_bisector, "", _("Use an alternative, more obvious bisection strategy: just chop 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, 400.0, true, false); + _canvas_new_bisector_size.init("/options/rendering/new_bisector_size", 1.0, 10000.0, 1.0, 0.0, 500.0, true, false); _page_rendering.add_line(true, _("New bisector tile size"), _canvas_new_bisector_size, C_("pixel abbreviation", "px"), _("Chop rectangles until largest dimension is this small"), false); _canvas_max_affine_diff.init("/options/rendering/max_affine_diff", 0.0, 100.0, 0.1, 0.0, 1.8, false, false); _page_rendering.add_line(true, _("Max affine diff"), _canvas_max_affine_diff, "", _("How much the viewing transformation can change before throwing away the current redraw and starting again"), false); _canvas_pad.init("/options/rendering/pad", 0.0, 1000.0, 1.0, 0.0, 200.0, true, false); _page_rendering.add_line(true, _("Buffer padding"), _canvas_pad, C_("pixel abbreviation", "px"), _("Use buffers bigger than the window by this amount"), false); _canvas_coarsener_min_size.init("/options/rendering/coarsener_min_size", 0.0, 1000.0, 1.0, 0.0, 200.0, true, false); - _page_rendering.add_line(true, _("Coarsener min size"), _canvas_coarsener_min_size, C_("pixel abbreviation", "px"), _("Parameter given to the coarsener algorithm when applied to the paint region. Probably best left alone!"), false); + _page_rendering.add_line(true, _("Coarsener min size"), _canvas_coarsener_min_size, C_("pixel abbreviation", "px"), _("Only coarsen rectangles smaller/thinner than this."), false); _canvas_coarsener_glue_size.init("/options/rendering/coarsener_glue_size", 0.0, 1000.0, 1.0, 0.0, 80.0, true, false); - _page_rendering.add_line(true, _("Coarsener glue size"), _canvas_coarsener_glue_size, C_("pixel abbreviation", "px"), _("Parameter given to the coarsener algorithm when applied to the paint region. Probably best left alone!"), false); + _page_rendering.add_line(true, _("Coarsener glue size"), _canvas_coarsener_glue_size, C_("pixel abbreviation", "px"), _("Absorb nearby rectangles within this distance."), false); + _canvas_coarsener_min_fullness.init("/options/rendering/coarsener_min_fullness", 0.0, 1.0, 0.0, 0.0, 0.3, false, false); + _page_rendering.add_line(true, _("Coarsener min fullness"), _canvas_coarsener_min_fullness, "", _("Refuse coarsening attempt if result would be more empty than this."), false); _page_rendering.add_group_header(_("Debugging, profiling, and experiments")); _canvas_debug_framecheck.init(_("Framecheck"), "/options/rendering/debug_framecheck", false); diff --git a/src/ui/dialog/inkscape-preferences.h b/src/ui/dialog/inkscape-preferences.h index cc95c1cd1a..919bc47cde 100644 --- a/src/ui/dialog/inkscape-preferences.h +++ b/src/ui/dialog/inkscape-preferences.h @@ -345,6 +345,7 @@ protected: UI::Widget::PrefSpinButton _canvas_pad; UI::Widget::PrefSpinButton _canvas_coarsener_min_size; UI::Widget::PrefSpinButton _canvas_coarsener_glue_size; + UI::Widget::PrefSpinButton _canvas_coarsener_min_fullness; UI::Widget::PrefCheckButton _canvas_debug_framecheck; UI::Widget::PrefCheckButton _canvas_debug_logging; diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 7e4627dbf9..544837f636 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -140,11 +140,12 @@ struct Prefs Pref update_strategy = Pref ("/options/rendering/update_strategy", 3, 1, 3); Pref render_time_limit = Pref ("/options/rendering/render_time_limit", 1000, 100, 1000000); Pref use_new_bisector = Pref ("/options/rendering/use_new_bisector", true); - Pref new_bisector_size = Pref ("/options/rendering/new_bisector_size", 400, 1, 10000); + Pref new_bisector_size = Pref ("/options/rendering/new_bisector_size", 500, 1, 10000); Pref max_affine_diff = Pref("/options/rendering/max_affine_diff", 1.8, 0.0, 100.0); Pref pad = Pref ("/options/rendering/pad", 200, 0, 1000); Pref coarsener_min_size = Pref ("/options/rendering/coarsener_min_size", 200, 0, 1000); Pref coarsener_glue_size = Pref ("/options/rendering/coarsener_glue_size", 80, 0, 1000); + Pref coarsener_min_fullness = Pref("/options/rendering/coarsener_min_fullness", 0.3, 0.0, 1.0); // Debug switches Pref debug_framecheck = Pref ("/options/rendering/debug_framecheck"); @@ -1331,7 +1332,7 @@ calc_affine_diff(const Geom::Affine &a, const Geom::Affine &b) { // 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) +coarsen(const Cairo::RefPtr ®ion, int min_size, int glue_size, double min_fullness) { // Sort the rects by minExtent struct Compare @@ -1350,42 +1351,75 @@ coarsen(const Cairo::RefPtr ®ion, int min_size, int glue_size) std::vector processed; processed.reserve(nrects); + // Removal lists. + std::vector remove_rects; + std::vector remove_processed; + // Repeatedly expand small rectangles by absorbing their nearby small rectangles. while (!rects.empty() && rects.begin()->minExtent() < min_size) { // Extract the smallest unprocessed rectangle. auto rect = *rects.begin(); rects.erase(rects.begin()); + // Initialise the effective glue size + int effective_glue_size = glue_size; + while (true) { // Find the glue zone. auto glue_zone = rect; - glue_zone.expandBy(glue_size); + glue_zone.expandBy(effective_glue_size); // Absorb rectangles in the glue zone. We could do better algorithmically speaking, but in real life it's already plenty fast. - auto orig = rect; - for (auto it = rects.begin(); it != rects.end(); ) { + auto newrect = rect; + int absorbed_area = 0; + + remove_rects.clear(); + for (auto it = rects.begin(); it != rects.end(); ++it) { if (glue_zone.contains(*it)) { - rect.unionWith(*it); - it = rects.erase(it); - } else { - ++it; + newrect.unionWith(*it); + absorbed_area += it->area(); + remove_rects.emplace_back(it); } } - for (auto it = processed.begin(); it != processed.end(); ) { - if (glue_zone.contains(*it)) { - rect.unionWith(*it); - *it = processed.back(); - processed.pop_back(); - } else { - ++it; + + remove_processed.clear(); + for (int i = 0; i < processed.size(); i++) { + auto &r = processed[i]; + if (glue_zone.contains(r)) { + newrect.unionWith(r); + absorbed_area += r.area(); + remove_processed.emplace_back(i); } } + // If the result was too empty, try again with a smaller glue size. + double fullness = (double)(rect.area() + absorbed_area) / newrect.area(); + if (fullness < min_fullness) { + effective_glue_size /= 2; + continue; + } + + // Commit the change + rect = newrect; + + for (auto &it : remove_rects) { + rects.erase(it); + } + + for (int j = (int)remove_processed.size() - 1; j >= 0; j--) { + int i = remove_processed[j]; + processed[i] = processed.back(); + processed.pop_back(); + } + // Stop growing if not changed or now big enough. - bool finished = rect == orig || rect.minExtent() >= min_size; + bool finished = absorbed_area == 0 || rect.minExtent() >= min_size; if (finished) { break; } + + // Otherwise, continue normally. + effective_glue_size = glue_size; } // Put the finished rectangle in processed. @@ -1710,7 +1744,7 @@ CanvasPrivate::on_idle() } // Get the list of rectangles to paint, coarsened to avoid fragmentation. - auto rects = coarsen(paint_region, prefs.coarsener_min_size, prefs.coarsener_glue_size); + auto rects = coarsen(paint_region, prefs.coarsener_min_size, prefs.coarsener_glue_size, prefs.coarsener_min_fullness); // Ensure that all the rectangles lie within the visible rect (and therefore within the store). #ifndef NDEBUG -- GitLab From 7fcdc1e0bab16ea627893710b02cf8cc8ca4b443 Mon Sep 17 00:00:00 2001 From: PBS Date: Tue, 25 Jan 2022 12:07:56 +0900 Subject: [PATCH 27/35] The return of X-ray mode / split view. --- src/ui/widget/canvas.cpp | 249 +++++++++++++++++++++++++++++---------- 1 file changed, 188 insertions(+), 61 deletions(-) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 544837f636..4cfa3eaacc 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -135,6 +135,7 @@ struct Prefs Pref x_ray_radius = Pref ("/options/rendering/xray-radius", 100, 1, 1500); Pref from_display = Pref ("/options/displayprofile/from_display"); Pref grabsize = Pref ("/options/grabsize/value", 3, 1, 15); + Pref outline_overlay_opacity = Pref ("/options/rendering/outline-overlay-opacity", 50, 1, 100); // New parameters Pref update_strategy = Pref ("/options/rendering/update_strategy", 3, 1, 3); @@ -417,13 +418,11 @@ public: // Important global properties of all the stores. If these change, all the stores must be recreated. int _device_scale = 1; bool _store_solid_background; - bool _have_outline_store; // The backing store. Geom::IntRect _store_rect; Geom::Affine _store_affine; - Cairo::RefPtr _backing_store; - Cairo::RefPtr _outline_store; + Cairo::RefPtr _backing_store, _outline_store; // The updater. Holds the clean region, the subregion of the store with up-to-date content, and some additional state determining in what order the unclean regions should be redrawn. std::unique_ptr updater; @@ -431,13 +430,14 @@ public: // The snapshot store. Used to mask redraw delay on zoom/rotate. Geom::IntRect _snapshot_rect; Geom::Affine _snapshot_affine; - Cairo::RefPtr _snapshot_store; + Cairo::RefPtr _snapshot_store, _snapshot_outline_store; Cairo::RefPtr _snapshot_clean_region; Geom::Affine geom_affine; // The affine the geometry was last imbued with. bool decoupled_mode = false; bool solid_background; // Whether the last background set is solid. + bool need_outline_store() const {return q->_split_mode != Inkscape::SplitMode::NORMAL || q->_drawing->outlineOverlay();} // Idle system callbacks. The high priority idle ensures at least one idle cycle between add_idle and on_draw. sigc::connection hipri_idle; @@ -548,7 +548,8 @@ Canvas::Canvas() 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.update_strategy.action = [=] {d->updater = make_updater(d->prefs.update_strategy, d->updater->clean_region);}; + d->prefs.update_strategy.action = [=] {d->updater = make_updater(d->prefs.update_strategy, std::move(d->updater->clean_region));}; + d->prefs.outline_overlay_opacity.action = [=] {queue_draw();}; // Give _pick_event an initial definition. _pick_event.type = GDK_LEAVE_NOTIFY; @@ -1143,12 +1144,12 @@ void Canvas::on_realize() * 1. Ensures that if the idle process was started, at least one cycle has run. * * 2. Blits the store(s) onto the canvas, clipping the outline store as required. - * (Or composites them with the transformed snapshot store in decoupled mode.) + * (Or composites them with the transformed snapshot store(s) in decoupled mode.) * * 3. Draws the "controller" in the 'split' split mode. */ bool -Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) +Canvas::on_draw(const Cairo::RefPtr<::Cairo::Context> &cr) { auto f = FrameCheck::Event(); @@ -1176,71 +1177,115 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->restore(); } - if (!d->decoupled_mode) { - // Blit backing store to screen. - if (d->prefs.debug_framecheck) f = FrameCheck::Event("draw"); - cr->save(); - cr->set_operator(d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); - cr->set_source(d->_backing_store, d->_store_rect.left() - _pos.x(), d->_store_rect.top() - _pos.y()); - cr->paint(); - cr->restore(); - } else { - // Turn off anti-aliasing for huge performance gains. Only applies to this compositing step. - cr->set_antialias(Cairo::ANTIALIAS_NONE); + auto draw_store = [&, this] (const Cairo::RefPtr &store, const Cairo::RefPtr &snapshot_store) { + if (!d->decoupled_mode) { + // Blit store to screen. + if (d->prefs.debug_framecheck) f = FrameCheck::Event("draw"); + cr->save(); + cr->set_operator(d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); + cr->set_source(store, d->_store_rect.left() - _pos.x(), d->_store_rect.top() - _pos.y()); + cr->paint(); + cr->restore(); + } else { + // Turn off anti-aliasing for huge performance gains. Only applies to this compositing step. + cr->set_antialias(Cairo::ANTIALIAS_NONE); + + // Blit background to complement of both clean regions, if solid (and therefore not already drawn). + if (d->solid_background) { + if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 2); + cr->save(); + cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD); + cr->rectangle(0, 0, get_allocation().get_width(), get_allocation().get_height()); + cr->translate(-_pos.x(), -_pos.y()); + cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); + region_to_path(cr, d->updater->clean_region); + cr->transform(geom_to_cairo(d->_store_affine * d->_snapshot_affine.inverse())); + region_to_path(cr, d->_snapshot_clean_region); + cr->clip(); + cr->set_source(_background); + cr->set_operator(Cairo::OPERATOR_SOURCE); + Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST); + cr->paint(); + cr->restore(); + } - // Blit background to complement of both clean regions, if solid (and therefore not already drawn). - if (d->solid_background) { - if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 2); + // Draw transformed snapshot, clipped to its clean region and the complement of the store's clean region. + if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 1); cr->save(); cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD); cr->rectangle(0, 0, get_allocation().get_width(), get_allocation().get_height()); cr->translate(-_pos.x(), -_pos.y()); cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); region_to_path(cr, d->updater->clean_region); + cr->clip(); cr->transform(geom_to_cairo(d->_store_affine * d->_snapshot_affine.inverse())); region_to_path(cr, d->_snapshot_clean_region); cr->clip(); - cr->set_source(_background); - cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->set_source(snapshot_store, d->_snapshot_rect.left(), d->_snapshot_rect.top()); + cr->set_operator(d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); + Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST); + cr->paint(); + if (d->prefs.debug_show_snapshot) { + cr->set_source_rgba(0, 0, 1, 0.2); + cr->set_operator(Cairo::OPERATOR_OVER); + cr->paint(); + } + cr->restore(); + + // Draw transformed store, clipped to clean region. + if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 0); + cr->save(); + cr->translate(-_pos.x(), -_pos.y()); + cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); + region_to_path(cr, d->updater->clean_region); + cr->clip(); + cr->set_source(store, d->_store_rect.left(), d->_store_rect.top()); + cr->set_operator(d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST); cr->paint(); cr->restore(); } + }; + + // Draw the backing store. + draw_store(d->_backing_store, d->_snapshot_store); + + // Draw overlay if required. + if (_drawing->outlineOverlay()) { + assert(d->_outline_store); + + double outline_overlay_opacity = 1.0 - d->prefs.outline_overlay_opacity / 100.0; + + // Partially obscure drawing by painting semi-transparent white. + cr->set_source_rgb(1.0, 1.0, 1.0); + cr->paint_with_alpha(outline_overlay_opacity); + + // Overlay outline. + draw_store(d->_outline_store, d->_snapshot_outline_store); + } + + // Draw split if required. + if (_split_mode != Inkscape::SplitMode::NORMAL) { + assert(d->_outline_store); + + // Move split position to center if not in canvas. + auto const rect = Geom::Rect(Geom::Point(), get_dimensions()); + if (!rect.contains(_split_position)) { + _split_position = rect.midpoint(); + } - // Draw transformed snapshot, clipped to its clean region and the complement of the backing store's clean region. - if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 1); + // Add clipping path and blit background. cr->save(); - cr->set_fill_rule(Cairo::FILL_RULE_EVEN_ODD); - cr->rectangle(0, 0, get_allocation().get_width(), get_allocation().get_height()); - cr->translate(-_pos.x(), -_pos.y()); - cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); - region_to_path(cr, d->updater->clean_region); - cr->clip(); - cr->transform(geom_to_cairo(d->_store_affine * d->_snapshot_affine.inverse())); - region_to_path(cr, d->_snapshot_clean_region); - cr->clip(); - cr->set_source(d->_snapshot_store, d->_snapshot_rect.left(), d->_snapshot_rect.top()); - cr->set_operator(d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); - Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST); + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->set_source(_background); + add_clippath(cr); cr->paint(); - if (d->prefs.debug_show_snapshot) { - cr->set_source_rgba(0, 0, 1, 0.2); - cr->set_operator(Cairo::OPERATOR_OVER); - cr->paint(); - } cr->restore(); - // Draw transformed store, clipped to clean region. - if (d->prefs.debug_framecheck) f = FrameCheck::Event("composite", 0); + // Add clipping path and draw outline store. cr->save(); - cr->translate(-_pos.x(), -_pos.y()); - cr->transform(geom_to_cairo(_affine * d->_store_affine.inverse())); - region_to_path(cr, d->updater->clean_region); - cr->clip(); - cr->set_source(d->_backing_store, d->_store_rect.left(), d->_store_rect.top()); - cr->set_operator(d->solid_background ? Cairo::OPERATOR_SOURCE : Cairo::OPERATOR_OVER); - Cairo::SurfacePattern(cr->get_source()->cobj()).set_filter(Cairo::FILTER_FAST); - cr->paint(); + add_clippath(cr); + draw_store(d->_outline_store, d->_snapshot_outline_store); cr->restore(); } @@ -1274,6 +1319,55 @@ Canvas::on_draw(const::Cairo::RefPtr<::Cairo::Context> &cr) cr->restore(); } + if (_split_mode == Inkscape::SplitMode::SPLIT) { + // Add dividing line. + cr->save(); + cr->set_source_rgb(0, 0, 0); + cr->set_line_width(1); + if (_split_direction == Inkscape::SplitDirection::EAST || + _split_direction == Inkscape::SplitDirection::WEST) { + cr->move_to((int)_split_position.x() + 0.5, 0); + cr->line_to((int)_split_position.x() + 0.5, get_dimensions().y()); + cr->stroke(); + } else { + cr->move_to( 0, (int)_split_position.y() + 0.5); + cr->line_to(get_dimensions().x(), (int)_split_position.y() + 0.5); + cr->stroke(); + } + cr->restore(); + + // Add controller image. + double a = _hover_direction == Inkscape::SplitDirection::NONE ? 0.5 : 1.0; + cr->save(); + cr->set_source_rgba(0.2, 0.2, 0.2, a); + cr->arc(_split_position.x(), _split_position.y(), 20 * d->_device_scale, 0, 2 * M_PI); + cr->fill(); + cr->restore(); + + cr->save(); + for (int i = 0; i < 4; ++i) { + // The four direction triangles. + cr->save(); + + // Position triangle. + cr->translate(_split_position.x(), _split_position.y()); + cr->rotate((i + 2) * M_PI / 2.0); + + // Draw triangle. + cr->move_to(-5 * d->_device_scale, 8 * d->_device_scale); + cr->line_to( 0, 18 * d->_device_scale); + cr->line_to( 5 * d->_device_scale, 8 * d->_device_scale); + cr->close_path(); + + double b = (int)_hover_direction == (i + 1) ? 0.9 : 0.7; + cr->set_source_rgba(b, b, b, a); + cr->fill(); + + cr->restore(); + } + cr->restore(); + } + // Process bucketed events as soon as possible after draw. if (d->pending_bucket_emptier) { if (!d->bucket.empty()) { @@ -1399,7 +1493,7 @@ coarsen(const Cairo::RefPtr ®ion, int min_size, int glue_size, continue; } - // Commit the change + // Commit the change. rect = newrect; for (auto &it : remove_rects) { @@ -1538,7 +1632,6 @@ CanvasPrivate::on_idle() int desired_width = _store_rect.width() * _device_scale; int desired_height = _store_rect.height() * _device_scale; if (!_backing_store || _backing_store->get_width() != desired_width || _backing_store->get_height() != desired_height) { - // Todo: Stop cairo unnecessarily pre-filling the store with transparency below if solid_background is true. _backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, desired_width, desired_height); cairo_surface_set_device_scale(_backing_store->cobj(), _device_scale, _device_scale); // No C++ API! } @@ -1550,12 +1643,21 @@ CanvasPrivate::on_idle() cr->set_operator(Cairo::OPERATOR_CLEAR); } cr->paint(); + if (need_outline_store()) { + if (!_outline_store || _outline_store->get_width() != desired_width || _outline_store->get_height() != desired_height) { + _outline_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, desired_width, desired_height); + cairo_surface_set_device_scale(_outline_store->cobj(), _device_scale, _device_scale); // No C++ API! + } + auto cr = Cairo::Context::create(_outline_store); + cr->set_operator(Cairo::OPERATOR_CLEAR); + cr->paint(); + } updater->reset(); if (prefs.debug_show_unclean) q->queue_draw(); }; // Determine whether the rendering parameters have changed, and reset if so. - if (!_backing_store || _device_scale != q->get_scale_factor() || _store_solid_background != solid_background) { + if (!_backing_store || (need_outline_store() && !_outline_store) || _device_scale != q->get_scale_factor() || _store_solid_background != solid_background) { _device_scale = q->get_scale_factor(); _store_solid_background = solid_background; recreate_store(); @@ -1563,11 +1665,15 @@ CanvasPrivate::on_idle() if (prefs.debug_logging) std::cout << "Full reset" << std::endl; } + // Make sure to clear the outline store when not in use, so we don't accidentally re-use it when it is required again. + if (!need_outline_store()) { + _outline_store.clear(); + } + auto shift_store = [&, this] { // Recreate the store, but keep re-usable content from the old store. auto store_rect = q->get_area_world(); store_rect.expandBy(pad); - // Todo: Stop cairo unnecessarily pre-filling the store with transparency below if solid_background is true. auto backing_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, store_rect.width() * _device_scale, store_rect.height() * _device_scale); cairo_surface_set_device_scale(backing_store->cobj(), _device_scale, _device_scale); // No C++ API! @@ -1586,8 +1692,6 @@ CanvasPrivate::on_idle() if (solid_background) { cr->set_operator(Cairo::OPERATOR_SOURCE); cr->set_source(q->_background); - } else { - cr->set_operator(Cairo::OPERATOR_CLEAR); } region_to_path(cr, reg); cr->fill(); @@ -1595,18 +1699,30 @@ CanvasPrivate::on_idle() } // Copy re-usuable contents of old store into new store, shifted. - cr->save(); cr->rectangle(reuse_rect->left() - store_rect.left(), reuse_rect->top() - store_rect.top(), reuse_rect->width(), reuse_rect->height()); cr->clip(); cr->set_source(_backing_store, -shift.x(), -shift.y()); cr->set_operator(Cairo::OPERATOR_SOURCE); cr->paint(); - cr->restore(); // Set the result as the new backing store. _store_rect = store_rect; assert(_store_affine == q->_affine); // Should not be called if the affine has changed. _backing_store = std::move(backing_store); + + // Do the same for the outline store + if (_outline_store) { + auto outline_store = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, store_rect.width() * _device_scale, store_rect.height() * _device_scale); + cairo_surface_set_device_scale(outline_store->cobj(), _device_scale, _device_scale); // No C++ API! + auto cr = Cairo::Context::create(outline_store); + cr->rectangle(reuse_rect->left() - store_rect.left(), reuse_rect->top() - store_rect.top(), reuse_rect->width(), reuse_rect->height()); + cr->clip(); + cr->set_source(_outline_store, -shift.x(), -shift.y()); + cr->set_operator(Cairo::OPERATOR_SOURCE); + cr->paint(); + _outline_store = std::move(outline_store); + } + updater->intersect(_store_rect); if (prefs.debug_show_unclean) q->queue_draw(); }; @@ -1618,6 +1734,9 @@ CanvasPrivate::on_idle() _snapshot_affine = _store_affine; _snapshot_clean_region = updater->clean_region->copy(); + // Do the same for the outline store + std::swap(_snapshot_outline_store, _outline_store); + // Recreate the backing store, making the state valid again. recreate_store(); }; @@ -1847,6 +1966,14 @@ CanvasPrivate::paint_rect_internal(Geom::IntRect const &rect) q->_drawing->setColorMode(q->_color_mode); paint_single_buffer(rect, _backing_store); + if (_outline_store) { + auto mode = q->_drawing->renderMode(); + q->_drawing->setRenderMode(Inkscape::RenderMode::OUTLINE); + q->_drawing->setColorMode(q->_color_mode); + paint_single_buffer(rect, _outline_store); + q->_drawing->setRenderMode(mode); + } + // Introduce an artificial delay for each rectangle. if (prefs.debug_slow_redraw) g_usleep(prefs.debug_slow_redraw_time); @@ -1889,7 +2016,7 @@ CanvasPrivate::paint_single_buffer(Geom::IntRect const &paint_rect, const Cairo: // Make sure the following code does not go outside of store's data assert(store); assert(store->get_format() == Cairo::FORMAT_ARGB32); - assert(_store_rect.contains(paint_rect)); + assert(_store_rect.contains(paint_rect)); // OBSERVED TO FAIL!!! Trigger was hitting Ctrl+O during a redraw, but cannot reproduce! // Create temporary surface that draws directly to store. store->flush(); -- GitLab From 437f771911318bcf1967ee67249b42130c000cd4 Mon Sep 17 00:00:00 2001 From: PBS Date: Tue, 25 Jan 2022 12:10:56 +0900 Subject: [PATCH 28/35] Update Copyrights. --- src/ui/widget/canvas.cpp | 7 +++---- src/ui/widget/canvas.h | 5 +++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 4cfa3eaacc..efe18fd5c2 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -1,12 +1,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Author: + * Authors: * Tavmjong Bah + * PBS * - * Rewrite of code originally in sp-canvas.cpp. - * - * Copyright (C) 2020 Tavmjong Bah + * Copyright (C) 2022 Authors * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ diff --git a/src/ui/widget/canvas.h b/src/ui/widget/canvas.h index e84e1703e9..cfbba97e42 100644 --- a/src/ui/widget/canvas.h +++ b/src/ui/widget/canvas.h @@ -3,10 +3,11 @@ #ifndef INKSCAPE_UI_WIDGET_CANVAS_H #define INKSCAPE_UI_WIDGET_CANVAS_H /* - * Author: + * Authors: * Tavmjong Bah + * PBS * - * Copyright (C) 2020 Tavmjong Bah + * Copyright (C) 2022 Authors * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ -- GitLab From 89f0df2d9bf2436f1ceb5fa8dfb0b30c797aa8a5 Mon Sep 17 00:00:00 2001 From: PBS Date: Tue, 25 Jan 2022 18:23:08 +0900 Subject: [PATCH 29/35] Cap coarsener parameters at half the tile size ...so that the algo doesn't get pushed outside of its design range by people experimenting too much, and end up doing stupid things :) Specifically, setting the coarsener min_size and glue_size larger than the tile size (they should be smaller) and turning on slow mode to see what's going on causes massively overlapping tiles to be generated. Some people might come away thinking that the algo must therefore be operating inefficiently all the time, when in fact it's just due to a terrible choice of parameters. So we pre-emptively avoid that confusion. --- src/ui/widget/canvas.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index efe18fd5c2..43e6cd4600 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -1837,8 +1837,6 @@ CanvasPrivate::on_idle() Gdk::ModifierType mask; window->get_device_position(Gdk::Display::get_default()->get_default_seat()->get_pointer(), x, y, mask); mouse_loc = Geom::IntPoint(x, y); - } else { - mouse_loc = Geom::IntPoint(); // Doesn't particularly matter, just as long as it's initialised. } // Map the mouse to canvas space. @@ -1862,7 +1860,10 @@ CanvasPrivate::on_idle() } // Get the list of rectangles to paint, coarsened to avoid fragmentation. - auto rects = coarsen(paint_region, prefs.coarsener_min_size, prefs.coarsener_glue_size, prefs.coarsener_min_fullness); + auto rects = coarsen(paint_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); // Ensure that all the rectangles lie within the visible rect (and therefore within the store). #ifndef NDEBUG -- GitLab From 0e42d4a98496c1877d33f431cdc110b35243760f Mon Sep 17 00:00:00 2001 From: PBS Date: Tue, 25 Jan 2022 19:04:15 +0900 Subject: [PATCH 30/35] Fix crash from the way outline mode was handled Crash was due to outline store not being initialised, because the criterion used by on_idle to determine whether it needed to be initialised was different from the one on_draw used to draw it. We also take this opportunity to clean up the way that this information was communicated (previously via a round trip from Document). --- src/ui/widget/canvas.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 43e6cd4600..b01723bb45 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -436,7 +436,7 @@ public: bool decoupled_mode = false; bool solid_background; // Whether the last background set is solid. - bool need_outline_store() const {return q->_split_mode != Inkscape::SplitMode::NORMAL || q->_drawing->outlineOverlay();} + bool need_outline_store() const {return q->_split_mode != Inkscape::SplitMode::NORMAL || q->_render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY;} // Idle system callbacks. The high priority idle ensures at least one idle cycle between add_idle and on_draw. sigc::connection hipri_idle; @@ -1250,7 +1250,7 @@ Canvas::on_draw(const Cairo::RefPtr<::Cairo::Context> &cr) draw_store(d->_backing_store, d->_snapshot_store); // Draw overlay if required. - if (_drawing->outlineOverlay()) { + if (_render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY) { assert(d->_outline_store); double outline_overlay_opacity = 1.0 - d->prefs.outline_overlay_opacity / 100.0; @@ -1962,16 +1962,13 @@ void CanvasPrivate::paint_rect_internal(Geom::IntRect const &rect) { // Paint the rectangle. - q->_drawing->setRenderMode(q->_render_mode); q->_drawing->setColorMode(q->_color_mode); + q->_drawing->setRenderMode(q->_render_mode); paint_single_buffer(rect, _backing_store); if (_outline_store) { - auto mode = q->_drawing->renderMode(); q->_drawing->setRenderMode(Inkscape::RenderMode::OUTLINE); - q->_drawing->setColorMode(q->_color_mode); paint_single_buffer(rect, _outline_store); - q->_drawing->setRenderMode(mode); } // Introduce an artificial delay for each rectangle. @@ -2261,7 +2258,7 @@ Canvas::pick_current_item(GdkEvent *event) // If in split mode, look at where cursor is to see if one should pick with outline mode. _drawing->setRenderMode(_render_mode); - if (_split_mode == Inkscape::SplitMode::SPLIT && !_drawing->outlineOverlay()) { + if (_split_mode == Inkscape::SplitMode::SPLIT && _render_mode != Inkscape::RenderMode::OUTLINE_OVERLAY) { if ((_split_direction == Inkscape::SplitDirection::NORTH && y > _split_position.y()) || (_split_direction == Inkscape::SplitDirection::SOUTH && y < _split_position.y()) || (_split_direction == Inkscape::SplitDirection::WEST && x > _split_position.x()) || -- GitLab From 65e8c60c24fbbcf7e9dbe13098c3a92489d3fc93 Mon Sep 17 00:00:00 2001 From: PBS Date: Tue, 25 Jan 2022 19:20:28 +0900 Subject: [PATCH 31/35] Final comment cleanups. --- src/ui/widget/canvas.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index b01723bb45..eb1d978a4c 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -820,9 +820,9 @@ Cairo::RefPtr Canvas::get_backing_store() const } void -Canvas::forced_redraws_start(int count, bool reset) +Canvas::forced_redraws_start(int, bool) { - // Todo: not used; remove when ready. + // Not used; remove when ready. } /** @@ -1376,8 +1376,6 @@ Canvas::on_draw(const Cairo::RefPtr<::Cairo::Context> &cr) } } - // Todo: Add back X-ray view. - // Notify the update strategy that another frame has passed. d->updater->frame(); @@ -2013,7 +2011,7 @@ CanvasPrivate::paint_single_buffer(Geom::IntRect const &paint_rect, const Cairo: // Make sure the following code does not go outside of store's data assert(store); assert(store->get_format() == Cairo::FORMAT_ARGB32); - assert(_store_rect.contains(paint_rect)); // OBSERVED TO FAIL!!! Trigger was hitting Ctrl+O during a redraw, but cannot reproduce! + assert(_store_rect.contains(paint_rect)); // FIXME: Observed to fail once when hitting Ctrl+O while Canvas was busy. Haven't managed to reproduce it. Doesn't mean it's fixed. // Create temporary surface that draws directly to store. store->flush(); -- GitLab From cff3090c5629344fa94b6da195e23ff88e33dc5c Mon Sep 17 00:00:00 2001 From: PBS Date: Fri, 28 Jan 2022 01:20:23 +0900 Subject: [PATCH 32/35] Move Gobblers to Canvas `gobble_key_events` and `gobble_motion_events` were gobbling events off GTK's event queue, not the event bucket. That means they were discarding all of next frame's events, rather than just the excess ones from the curernt frame. To make them work again, we have them call into Canvas to gobble the corrcet events. * Also removed a duplicate function of `gobble_key_events`. * And removed forced redraws. --- src/gradient-drag.cpp | 6 +-- src/rubberband.cpp | 5 -- src/ui/knot/knot.cpp | 3 +- src/ui/tool/control-point-selection.cpp | 4 +- src/ui/tool/control-point.cpp | 16 +----- src/ui/tool/event-utils.cpp | 23 -------- src/ui/tool/event-utils.h | 1 - src/ui/tools/arc-tool.cpp | 6 --- src/ui/tools/box3d-tool.cpp | 4 -- src/ui/tools/calligraphic-tool.cpp | 2 - src/ui/tools/eraser-tool.cpp | 2 - src/ui/tools/node-tool.cpp | 4 -- src/ui/tools/pen-tool.cpp | 6 --- src/ui/tools/rect-tool.cpp | 6 --- src/ui/tools/select-tool.cpp | 15 ++---- src/ui/tools/select-tool.h | 6 +-- src/ui/tools/spiral-tool.cpp | 6 --- src/ui/tools/spray-tool.cpp | 3 -- src/ui/tools/star-tool.cpp | 5 -- src/ui/tools/tool-base.cpp | 69 +++--------------------- src/ui/tools/tool-base.h | 7 +-- src/ui/tools/tweak-tool.cpp | 2 - src/ui/widget/canvas.cpp | 70 ++++++++++++++++++++----- src/ui/widget/canvas.h | 7 +-- src/ui/widget/rotateable.cpp | 1 - 25 files changed, 81 insertions(+), 198 deletions(-) diff --git a/src/gradient-drag.cpp b/src/gradient-drag.cpp index 49cbe5c295..14d9edbded 100644 --- a/src/gradient-drag.cpp +++ b/src/gradient-drag.cpp @@ -1062,8 +1062,6 @@ static void gr_knot_mousedown_handler(SPKnot */*knot*/, unsigned int /*state*/, if (dragger_corner) { dragger_corner->highlightCorner(true); } - - dragger->parent->desktop->getCanvas()->forced_redraws_start(5); } /** @@ -1073,8 +1071,6 @@ static void gr_knot_ungrabbed_handler(SPKnot *knot, unsigned int state, gpointer { GrDragger *dragger = (GrDragger *) data; - dragger->parent->desktop->getCanvas()->forced_redraws_stop(); - dragger->point_original = dragger->point = knot->pos; if ((state & GDK_CONTROL_MASK) && (state & GDK_SHIFT_MASK)) { @@ -2822,7 +2818,7 @@ bool GrDrag::key_press_handler(GdkEvent *event) y_dir *= -desktop->yaxisdir(); - gint mul = 1 + Inkscape::UI::Tools::gobble_key_events(keyval, 0); // with any mask + gint mul = 1 + desktop->canvas->gobble_key_events(keyval, 0); // with any mask if (MOD__SHIFT(event)) { mul *= 10; diff --git a/src/rubberband.cpp b/src/rubberband.cpp index f31aecc98a..2d03bf0701 100644 --- a/src/rubberband.cpp +++ b/src/rubberband.cpp @@ -57,7 +57,6 @@ void Inkscape::Rubberband::start(SPDesktop *d, Geom::Point const &p) _points.push_back(_desktop->d2w(p)); delete_canvas_items(); - _desktop->getCanvas()->forced_redraws_start(5); } void Inkscape::Rubberband::stop() @@ -69,10 +68,6 @@ void Inkscape::Rubberband::stop() _touchpath_curve->reset(); delete_canvas_items(); - - if (_desktop && _desktop->getCanvas()) { - _desktop->getCanvas()->forced_redraws_stop(); - } } void Inkscape::Rubberband::move(Geom::Point const &p) diff --git a/src/ui/knot/knot.cpp b/src/ui/knot/knot.cpp index ada373fe0d..be1601f03d 100644 --- a/src/ui/knot/knot.cpp +++ b/src/ui/knot/knot.cpp @@ -30,6 +30,7 @@ #include "display/control/canvas-item-ctrl.h" #include "ui/tools/tool-base.h" #include "ui/tools/node-tool.h" +#include "ui/widget/canvas.h" using Inkscape::DocumentUndo; @@ -349,7 +350,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) { - Inkscape::UI::Tools::gobble_motion_events(GDK_BUTTON1_MASK); + knot->desktop->canvas->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 2ee5d617d9..e968e1c1af 100644 --- a/src/ui/tool/control-point-selection.cpp +++ b/src/ui/tool/control-point-selection.cpp @@ -19,6 +19,8 @@ #include "ui/tool/transform-handle-set.h" #include "ui/tool/node.h" #include "display/control/snap-indicator.h" +#include "ui/tools/tool-base.h" +#include "ui/widget/canvas.h" @@ -515,7 +517,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 + combine_key_events(shortcut_key(event), 0); + unsigned num = 1 + _desktop->canvas->gobble_key_events(shortcut_key(event), 0); Geom::Point delta = dir * num; if (held_shift(event)) delta *= 10; diff --git a/src/ui/tool/control-point.cpp b/src/ui/tool/control-point.cpp index 13f2e60c6e..929e9d0724 100644 --- a/src/ui/tool/control-point.cpp +++ b/src/ui/tool/control-point.cpp @@ -274,11 +274,7 @@ bool ControlPoint::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, G _drag_origin = _position; transferred = grabbed(&event->motion); // _drag_initiated might change during the above virtual call - if (!_drag_initiated) { - // this guarantees smooth redraws while dragging - _desktop->getCanvas()->forced_redraws_start(5); - _drag_initiated = true; - } + _drag_initiated = true; } if (!transferred) { @@ -318,7 +314,6 @@ bool ControlPoint::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, G if (_drag_initiated) { // it is the end of a drag - _desktop->getCanvas()->forced_redraws_stop(); _drag_initiated = false; ungrabbed(&event->button); return true; @@ -345,9 +340,6 @@ bool ControlPoint::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, G if (_event_grab && !event->grab_broken.keyboard) { { ungrabbed(nullptr); - if (_drag_initiated) { - _desktop->getCanvas()->forced_redraws_stop(); - } } _setState(STATE_NORMAL); _event_grab = false; @@ -394,7 +386,6 @@ bool ControlPoint::_eventHandler(Inkscape::UI::Tools::ToolBase *event_context, G _canvas_item_ctrl->ungrab(); _clearMouseover(); // this will also reset state to normal - _desktop->getCanvas()->forced_redraws_stop(); _event_grab = false; _drag_initiated = false; @@ -511,10 +502,7 @@ void ControlPoint::transferGrab(ControlPoint *prev_point, GdkEventMotion *event) prev_point->_canvas_item_ctrl->ungrab(); _canvas_item_ctrl->grab(_grab_event_mask, nullptr); // cursor is null - if (!_drag_initiated) { - _desktop->getCanvas()->forced_redraws_start(5); - _drag_initiated = true; - } + _drag_initiated = true; prev_point->_setState(STATE_NORMAL); _setMouseover(this, event->state); diff --git a/src/ui/tool/event-utils.cpp b/src/ui/tool/event-utils.cpp index 5008c127f1..f131d4f57f 100644 --- a/src/ui/tool/event-utils.cpp +++ b/src/ui/tool/event-utils.cpp @@ -31,29 +31,6 @@ guint shortcut_key(GdkEventKey const &event) return shortcut_key; } -unsigned combine_key_events(guint keyval, gint 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; -} - /** Returns the modifier state valid after this event. Use this when you process events * that change the modifier state. Currently handles only Shift, Ctrl, Alt. */ unsigned state_after_event(GdkEvent *event) diff --git a/src/ui/tool/event-utils.h b/src/ui/tool/event-utils.h index 6b303c9559..37961e39e2 100644 --- a/src/ui/tool/event-utils.h +++ b/src/ui/tool/event-utils.h @@ -110,7 +110,6 @@ inline bool held_button(E const &event) { } guint shortcut_key(GdkEventKey const &event); -unsigned combine_key_events(guint keyval, gint mask); unsigned state_after_event(GdkEvent *event); } // namespace UI diff --git a/src/ui/tools/arc-tool.cpp b/src/ui/tools/arc-tool.cpp index aac4430462..20227e4f1f 100644 --- a/src/ui/tools/arc-tool.cpp +++ b/src/ui/tools/arc-tool.cpp @@ -331,8 +331,6 @@ void ArcTool::drag(Geom::Point pt, guint state) { Inkscape::GC::release(repr); this->arc->transform = layer->i2doc_affine().inverse(); this->arc->updateRepr(); - - forced_redraws_start(5); } auto confine = Modifiers::Modifier::get(Modifiers::Type::TRANS_CONFINE)->active(state); @@ -429,8 +427,6 @@ void ArcTool::finishItem() { this->arc->updateRepr(); this->arc->doWriteTransform(this->arc->transform, nullptr, true); - forced_redraws_stop(); - desktop->getSelection()->set(this->arc); DocumentUndo::done(desktop->getDocument(), _("Create ellipse"), INKSCAPE_ICON("draw-ellipse")); @@ -453,8 +449,6 @@ void ArcTool::cancel() { this->yp = 0; this->item_to_select = nullptr; - forced_redraws_stop(); - DocumentUndo::cancel(desktop->getDocument()); } diff --git a/src/ui/tools/box3d-tool.cpp b/src/ui/tools/box3d-tool.cpp index 30780bd039..7b93dac070 100644 --- a/src/ui/tools/box3d-tool.cpp +++ b/src/ui/tools/box3d-tool.cpp @@ -524,8 +524,6 @@ void Box3dTool::drag(guint /*state*/) { // TODO: It would be nice to show the VPs during dragging, but since there is no selection // at this point (only after finishing the box), we must do this "manually" /* this._vpdrag->updateDraggers(); */ - - forced_redraws_start(5); } g_assert(this->box3d); @@ -564,8 +562,6 @@ void Box3dTool::finishItem() { this->box3d->relabel_corners(); - forced_redraws_stop(); - desktop->getSelection()->set(this->box3d); DocumentUndo::done(desktop->getDocument(), _("Create 3D box"), INKSCAPE_ICON("draw-cuboid")); diff --git a/src/ui/tools/calligraphic-tool.cpp b/src/ui/tools/calligraphic-tool.cpp index 2181d88050..826f4d4d98 100644 --- a/src/ui/tools/calligraphic-tool.cpp +++ b/src/ui/tools/calligraphic-tool.cpp @@ -475,7 +475,6 @@ bool CalligraphicTool::root_handler(GdkEvent* event) { ret = TRUE; - forced_redraws_start(3); set_high_motion_precision(); this->is_drawing = true; this->just_started_drawing = true; @@ -751,7 +750,6 @@ bool CalligraphicTool::root_handler(GdkEvent* event) { ungrabCanvasEvents(); - forced_redraws_stop(); set_high_motion_precision(false); this->is_drawing = false; diff --git a/src/ui/tools/eraser-tool.cpp b/src/ui/tools/eraser-tool.cpp index 7fafe290be..c00b4f20a1 100644 --- a/src/ui/tools/eraser-tool.cpp +++ b/src/ui/tools/eraser-tool.cpp @@ -414,7 +414,6 @@ bool EraserTool::root_handler(GdkEvent* event) { ret = TRUE; - forced_redraws_start(3); this->is_drawing = true; } break; @@ -458,7 +457,6 @@ bool EraserTool::root_handler(GdkEvent* event) { ungrabCanvasEvents(); - forced_redraws_stop(); this->is_drawing = false; if (this->dragging && event->button.button == 1) { diff --git a/src/ui/tools/node-tool.cpp b/src/ui/tools/node-tool.cpp index 86e7ee2b79..8614a03264 100644 --- a/src/ui/tools/node-tool.cpp +++ b/src/ui/tools/node-tool.cpp @@ -156,8 +156,6 @@ NodeTool::~NodeTool() { delete data.outline_group; delete data.dragpoint_group; delete _transform_handle_group; - - forced_redraws_stop(); } void NodeTool::setup() { @@ -449,8 +447,6 @@ bool NodeTool::root_handler(GdkEvent* event) { */ using namespace Inkscape::UI; // pull in event helpers - forced_redraws_start(5); - Inkscape::Selection *selection = desktop->selection; static Inkscape::Preferences *prefs = Inkscape::Preferences::get(); diff --git a/src/ui/tools/pen-tool.cpp b/src/ui/tools/pen-tool.cpp index 423111ef92..821f3ef695 100644 --- a/src/ui/tools/pen-tool.cpp +++ b/src/ui/tools/pen-tool.cpp @@ -161,8 +161,6 @@ void PenTool::_cancel() { cl1->hide(); this->message_context->clear(); this->message_context->flash(Inkscape::NORMAL_MESSAGE, _("Drawing cancelled")); - - forced_redraws_stop(); } /** @@ -1214,8 +1212,6 @@ void PenTool::_setInitialPoint(Geom::Point const p) { this->p[1] = p; this->npoints = 2; this->red_bpath->set_bpath(nullptr); - - forced_redraws_start(5); } /** @@ -1962,8 +1958,6 @@ void PenTool::_finish(gboolean const closed) { this->green_anchor.reset(); - forced_redraws_stop(); - this->_enableEvents(); } diff --git a/src/ui/tools/rect-tool.cpp b/src/ui/tools/rect-tool.cpp index 2b575d28b4..19f60823df 100644 --- a/src/ui/tools/rect-tool.cpp +++ b/src/ui/tools/rect-tool.cpp @@ -364,8 +364,6 @@ void RectTool::drag(Geom::Point const pt, guint state) { this->rect->transform = currentLayer()->i2doc_affine().inverse(); this->rect->updateRepr(); - - forced_redraws_start(5); } Geom::Rect const r = Inkscape::snap_rectangular_box(desktop, this->rect, pt, this->center, state); @@ -446,8 +444,6 @@ void RectTool::finishItem() { this->rect->updateRepr(); this->rect->doWriteTransform(this->rect->transform, nullptr, true); - forced_redraws_stop(); - this->desktop->getSelection()->set(this->rect); DocumentUndo::done(this->desktop->getDocument(), _("Create rectangle"), INKSCAPE_ICON("draw-rectangle")); @@ -470,8 +466,6 @@ void RectTool::cancel(){ this->yp = 0; this->item_to_select = nullptr; - forced_redraws_stop(); - DocumentUndo::cancel(this->desktop->getDocument()); } diff --git a/src/ui/tools/select-tool.cpp b/src/ui/tools/select-tool.cpp index a3568ef515..61f535f2b7 100644 --- a/src/ui/tools/select-tool.cpp +++ b/src/ui/tools/select-tool.cpp @@ -65,13 +65,13 @@ namespace Tools { static gint rb_escaped = 0; // if non-zero, rubberband was canceled by esc, so the next button release should not deselect static gint drag_escaped = 0; // if non-zero, drag was canceled by esc +static bool is_cycling = false; const std::string& SelectTool::getPrefsPath() { - return SelectTool::prefsPath; + static std::string prefsPath = "/tools/select"; + return prefsPath; } -const std::string SelectTool::prefsPath = "/tools/select"; - SelectTool::SelectTool() : ToolBase("select.svg") , dragging(false) @@ -86,12 +86,6 @@ SelectTool::SelectTool() { } -//static gint xp = 0, yp = 0; // where drag started -//static gint tolerance = 0; -//static bool within_tolerance = false; -static bool is_cycling = false; - - SelectTool::~SelectTool() { this->enableGrDrag(false); @@ -111,8 +105,6 @@ SelectTool::~SelectTool() { sp_object_unref(item); item = nullptr; } - - forced_redraws_stop(); } void SelectTool::setup() { @@ -445,7 +437,6 @@ bool SelectTool::root_handler(GdkEvent* event) { if (this->item && this->item->document == nullptr) { this->sp_select_context_abort(); } - forced_redraws_start(5); switch (event->type) { case GDK_2BUTTON_PRESS: diff --git a/src/ui/tools/select-tool.h b/src/ui/tools/select-tool.h index eb4a5cb01c..e73a082fc8 100644 --- a/src/ui/tools/select-tool.h +++ b/src/ui/tools/select-tool.h @@ -42,12 +42,10 @@ public: bool cycling_wrap; SPItem *item; - Inkscape::CanvasItem *grabbed = nullptr; + Inkscape::CanvasItem *grabbed = nullptr; Inkscape::SelTrans *_seltrans; Inkscape::SelectionDescriber *_describer; - gchar *no_selection_msg = nullptr; - - static const std::string prefsPath; + gchar *no_selection_msg = nullptr; void setup() override; void set(const Inkscape::Preferences::Entry& val) override; diff --git a/src/ui/tools/spiral-tool.cpp b/src/ui/tools/spiral-tool.cpp index 38a862933f..786f63a322 100644 --- a/src/ui/tools/spiral-tool.cpp +++ b/src/ui/tools/spiral-tool.cpp @@ -335,8 +335,6 @@ void SpiralTool::drag(Geom::Point const &p, guint state) { Inkscape::GC::release(repr); this->spiral->transform = currentLayer()->i2doc_affine().inverse(); this->spiral->updateRepr(); - - forced_redraws_start(5); } SnapManager &m = desktop->namedview->snap_manager; @@ -387,8 +385,6 @@ void SpiralTool::finishItem() { spiral->updateRepr(SP_OBJECT_WRITE_EXT); spiral->doWriteTransform(spiral->transform, nullptr, true); - forced_redraws_stop(); - this->desktop->getSelection()->set(this->spiral); DocumentUndo::done(this->desktop->getDocument(), _("Create spiral"), INKSCAPE_ICON("draw-spiral")); @@ -411,8 +407,6 @@ void SpiralTool::cancel() { this->yp = 0; this->item_to_select = nullptr; - forced_redraws_stop(); - DocumentUndo::cancel(this->desktop->getDocument()); } diff --git a/src/ui/tools/spray-tool.cpp b/src/ui/tools/spray-tool.cpp index d7b5ac7c58..225a53a284 100644 --- a/src/ui/tools/spray-tool.cpp +++ b/src/ui/tools/spray-tool.cpp @@ -1257,7 +1257,6 @@ bool SprayTool::root_handler(GdkEvent* event) { sp_spray_extinput(this, event); - forced_redraws_start(3); set_high_motion_precision(); this->is_drawing = true; this->is_dilating = true; @@ -1331,7 +1330,6 @@ bool SprayTool::root_handler(GdkEvent* event) { } this->last_push = desktop->dt2doc(scroll_dt); sp_spray_extinput(this, event); - forced_redraws_start(3); this->is_drawing = true; this->is_dilating = true; this->has_dilated = false; @@ -1359,7 +1357,6 @@ bool SprayTool::root_handler(GdkEvent* event) { Geom::Point const motion_w(event->button.x, event->button.y); Geom::Point const motion_dt(desktop->w2d(motion_w)); - forced_redraws_stop(); set_high_motion_precision(false); this->is_drawing = false; diff --git a/src/ui/tools/star-tool.cpp b/src/ui/tools/star-tool.cpp index 6c60c1cb8c..6cdd80db31 100644 --- a/src/ui/tools/star-tool.cpp +++ b/src/ui/tools/star-tool.cpp @@ -351,8 +351,6 @@ void StarTool::drag(Geom::Point p, guint state) Inkscape::GC::release(repr); this->star->transform = currentLayer()->i2doc_affine().inverse(); this->star->updateRepr(); - - forced_redraws_start(5); } /* Snap corner point with no constraints */ @@ -407,7 +405,6 @@ void StarTool::finishItem() { this->star->set_shape(); this->star->updateRepr(SP_OBJECT_WRITE_EXT); this->star->doWriteTransform(this->star->transform, nullptr, true); - forced_redraws_stop(); desktop->getSelection()->set(this->star); DocumentUndo::done(desktop->getDocument(), _("Create star"), INKSCAPE_ICON("draw-polygon-star")); @@ -430,8 +427,6 @@ void StarTool::cancel() { this->yp = 0; this->item_to_select = nullptr; - forced_redraws_stop(); - DocumentUndo::cancel(desktop->getDocument()); } diff --git a/src/ui/tools/tool-base.cpp b/src/ui/tools/tool-base.cpp index 7594094076..dcca78f214 100644 --- a/src/ui/tools/tool-base.cpp +++ b/src/ui/tools/tool-base.cpp @@ -123,7 +123,6 @@ void ToolBase::setup() { } void ToolBase::finish() { - this->desktop->getCanvas()->forced_redraws_stop(); this->enableSelectionCue(false); } @@ -191,56 +190,6 @@ 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, gint 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. - */ -gint gobble_motion_events(gint 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_MOTION_NOTIFY - && (event_next->motion.state & mask)) { - // kill it - gdk_event_free(event_next); - // get next - event_next = gdk_event_get(); - i++; - } - // otherwise, put it back onto the queue - if (event_next) - gdk_event_put(event_next); - - return i; -} - /** * Toggles current tool between active tool and selector tool. * Subroutine of sp_event_context_private_root_handler(). @@ -311,7 +260,7 @@ static gdouble accelerate_scroll(GdkEvent *event, gdouble acceleration) bool ToolBase::_keyboardMove(GdkEventKey const &event, Geom::Point const &dir) { if (held_control(event)) return false; - unsigned num = 1 + combine_key_events(shortcut_key(event), 0); + unsigned num = 1 + gobble_key_events(shortcut_key(event), 0); Geom::Point delta = dir * num; if (held_shift(event)) { @@ -1117,27 +1066,23 @@ void ToolBase::set_high_motion_precision(bool high_precision) { } /** - * Force canvas to fully update after interruptions. + * Discard and count matching key events from top of event bucket. * Convenience function that just passes request to canvas. */ -void -ToolBase::forced_redraws_start(int count, bool reset) +int ToolBase::gobble_key_events(guint keyval, guint mask) const { - desktop->canvas->forced_redraws_start(count, reset); + return desktop->canvas->gobble_key_events(keyval, mask); } - /** - * End force canvas full updates. + * Discard matching motion events from top of event bucket. * Convenience function that just passes request to canvas. */ -void -ToolBase::forced_redraws_stop() +void ToolBase::gobble_motion_events(guint mask) const { - desktop->canvas->forced_redraws_stop(); + 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 9a10b85eda..f2ede71a90 100644 --- a/src/ui/tools/tool-base.h +++ b/src/ui/tools/tool-base.h @@ -252,8 +252,8 @@ protected: void set_high_motion_precision(bool high_precision = true); - void forced_redraws_start(int count, bool reset = false); - void forced_redraws_stop(); + int gobble_key_events(guint keyval, guint mask) const; + void gobble_motion_events(guint mask) const; SPDesktop *desktop = nullptr; @@ -271,9 +271,6 @@ gint sp_event_context_virtual_item_handler(ToolBase *ec, SPItem *item, GdkEvent void sp_event_root_menu_popup(SPDesktop *desktop, SPItem *item, GdkEvent *event); -gint gobble_key_events(guint keyval, gint mask); -gint gobble_motion_events(gint mask); - void sp_event_show_modifier_tip(Inkscape::MessageContext *message_context, GdkEvent *event, gchar const *ctrl_tip, gchar const *shift_tip, gchar const *alt_tip); diff --git a/src/ui/tools/tweak-tool.cpp b/src/ui/tools/tweak-tool.cpp index 36c1409586..4b5474c28f 100644 --- a/src/ui/tools/tweak-tool.cpp +++ b/src/ui/tools/tweak-tool.cpp @@ -1158,7 +1158,6 @@ bool TweakTool::root_handler(GdkEvent* event) { sp_tweak_extinput(this, event); - forced_redraws_start(3); this->is_drawing = true; this->is_dilating = true; this->has_dilated = false; @@ -1207,7 +1206,6 @@ bool TweakTool::root_handler(GdkEvent* event) { Geom::Point const motion_w(event->button.x, event->button.y); Geom::Point const motion_dt(desktop->w2d(motion_w)); - forced_redraws_stop(); this->is_drawing = false; if (this->is_dilating && event->button.button == 1) { diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index eb1d978a4c..85f6bc2da3 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -448,6 +448,7 @@ public: struct GdkEventFreer {void operator()(GdkEvent *ev) const {gdk_event_free(ev);}}; std::vector> bucket; sigc::connection bucket_emptier; + int bucket_pos; bool pending_bucket_emptier = false; bool idle_running = false; @@ -462,7 +463,6 @@ public: // Preferences system Prefs prefs; - void update_ctrl_sizes(int size) {q->_canvas_item_root->update_canvas_item_ctrl_sizes(size);} }; void CanvasPrivate::schedule_bucket_emptier() @@ -482,9 +482,13 @@ void CanvasPrivate::empty_bucket() pending_bucket_emptier = false; - auto bucket2 = std::move(bucket); + bucket_pos = 0; + + while (bucket_pos < bucket.size()) { + // Extract next event. + auto event = std::move(bucket[bucket_pos]); + bucket_pos++; - for (auto &event : bucket2) { // Block undo/redo while anything is dragged. if (event->type == GDK_BUTTON_PRESS && event->button.button == 1) { q->_is_dragging = true; @@ -517,6 +521,52 @@ void CanvasPrivate::empty_bucket() ignore = nullptr; } } + + bucket.clear(); +} + +// Used by some tools to batch backlogs of key events that may have built up after a freeze. Called during 'empty_bucket'. +int Canvas::gobble_key_events(guint keyval, guint mask) const +{ + int count = 0; + + while (d->bucket_pos < d->bucket.size()) { + auto &event = d->bucket[d->bucket_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 processing. + if (event->type == GDK_KEY_PRESS) count++; + d->bucket_pos++; + } + else { + // Stop discarding. + break; + } + } + + if (count > 0 && d->prefs.debug_logging) std::cout << "Gobbled " << count << " key presses" << std::endl; + + return count; +} + +// Used by some tools to ignore backlogs of motion events that may have built up after a freeze. Called during 'empty_bucket'. +void Canvas::gobble_motion_events(guint mask) const +{ + int count = 0; + + while (d->bucket_pos < d->bucket.size()) { + auto &event = d->bucket[d->bucket_pos]; + if (event->type == GDK_MOTION_NOTIFY && (event->motion.state & mask)) { + // Discard event and continue processing. + count++; + d->bucket_pos++; + } + else { + // Stop discarding. + break; + } + } + + if (count > 0 && d->prefs.debug_logging) std::cout << "Gobbled " << count << " motion events" << std::endl; } void CanvasPrivate::queue_draw_area(Geom::IntRect &rect) @@ -816,13 +866,7 @@ Canvas::set_split_direction(Inkscape::SplitDirection dir) Cairo::RefPtr Canvas::get_backing_store() const { - return d->_backing_store; -} - -void -Canvas::forced_redraws_start(int, bool) -{ - // Not used; remove when ready. + return d->_backing_store; } /** @@ -1844,6 +1888,7 @@ CanvasPrivate::on_idle() } // Begin processing redraws. + auto start_time = g_get_monotonic_time(); while (true) { // Get the clean region for the next redraw as reported by the updater. auto clean_region = updater->get_next_clean_region(); @@ -1877,7 +1922,6 @@ CanvasPrivate::on_idle() std::make_heap(rects.begin(), rects.end(), cmp); // Process rectangles until none left or timed out. - auto start_time = g_get_monotonic_time(); while (!rects.empty()) { // Extract the closest rectangle to the mouse. std::pop_heap(rects.begin(), rects.end(), cmp); @@ -2008,7 +2052,7 @@ CanvasPrivate::paint_rect_internal(Geom::IntRect const &rect) void CanvasPrivate::paint_single_buffer(Geom::IntRect const &paint_rect, const Cairo::RefPtr &store) { - // Make sure the following code does not go outside of store's data + // Make sure the following code does not go outside of store's data. assert(store); assert(store->get_format() == Cairo::FORMAT_ARGB32); assert(_store_rect.contains(paint_rect)); // FIXME: Observed to fail once when hitting Ctrl+O while Canvas was busy. Haven't managed to reproduce it. Doesn't mean it's fixed. @@ -2055,7 +2099,7 @@ CanvasPrivate::paint_single_buffer(Geom::IntRect const &paint_rect, const Cairo: q->_canvas_item_root->render(&buf); } - // Paint over newly drawn content with a translucent random colour + // Paint over newly drawn content with a translucent random colour. if (prefs.debug_show_redraw) { cr->set_source_rgba((rand() % 255) / 255.0, (rand() % 255) / 255.0, (rand() % 255) / 255.0, 0.2); cr->set_operator(Cairo::OPERATOR_OVER); diff --git a/src/ui/widget/canvas.h b/src/ui/widget/canvas.h index cfbba97e42..deea8a367b 100644 --- a/src/ui/widget/canvas.h +++ b/src/ui/widget/canvas.h @@ -93,8 +93,8 @@ public: Cairo::RefPtr get_backing_store() const; // Background rotation preview Cairo::RefPtr get_background_pattern() const { return _background; } - void forced_redraws_start(int count, bool reset = true); - void forced_redraws_stop() { _forced_redraw_limit = -1; } + int gobble_key_events(guint keyval, guint mask) const; // tool-base.cpp + void gobble_motion_events(guint mask) const; // tool-base.cpp // Canvas Items CanvasItemGroup *get_canvas_item_root() const { return _canvas_item_root; } @@ -191,9 +191,6 @@ private: std::string _cms_key; bool _cms_active = false; - int _forced_redraw_limit = -1; - int _forced_redraw_count = 0; - // Some objects (e.g. grids) when destroyed will request redraws. We need to block them when canvas // is destructed. (Windows are destroyed before documents as a document may have several windows. // Changes to documents should not be triggering changes to closed windows. This fix is a hack.) diff --git a/src/ui/widget/rotateable.cpp b/src/ui/widget/rotateable.cpp index 639f8d1a86..7f32823c7e 100644 --- a/src/ui/widget/rotateable.cpp +++ b/src/ui/widget/rotateable.cpp @@ -104,7 +104,6 @@ bool Rotateable::on_motion(GdkEventMotion *event) { do_motion(force, modifier); } } - Inkscape::UI::Tools::gobble_motion_events(GDK_BUTTON1_MASK); return true; } return false; -- GitLab From 466cc91a905dd10103de4436aa8c7e5a17a27527 Mon Sep 17 00:00:00 2001 From: PBS Date: Fri, 28 Jan 2022 18:36:45 +0900 Subject: [PATCH 33/35] Big commit fixing event system This commit correctly integrates the old events system with the event bucket logic. Previously, the lack of such integration caused bits of events handlers to run before bucketing and bits to run after. There was also no care as to the state that these event handlers manipulate. Now they are carefully split apart, with the state mutated by the event handlers occurring entirely in the delayed part of the event bucket. We also vastly reorganise and clean up the event pipeline. Fixes, among other things, broken enter and leave events. Side effects: previously coordinate transformations in decoupled mode mostly worked, apart from selection. This was an illusion. In fact it was doubly broken, cancelling out. Now we have fixed the event coordinate transformations, this exposes the brokenness of the w2d family of coordinate transformation functions. These will have to be gone through and fixed at some point. --- src/ui/widget/canvas.cpp | 1449 ++++++++++++++++++++------------------ src/ui/widget/canvas.h | 4 - 2 files changed, 767 insertions(+), 686 deletions(-) diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 85f6bc2da3..a8f66bf055 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -79,7 +79,9 @@ namespace Inkscape { namespace UI { namespace Widget { -// Preferences system +/* + * Preferences system + */ template struct PrefBase @@ -160,7 +162,9 @@ struct Prefs Pref debug_sticky_decoupled = Pref ("/options/rendering/debug_sticky_decoupled"); }; -// Conversion functions +/* + * Conversion functions + */ auto geom_to_cairo(Geom::IntRect rect) { @@ -192,9 +196,11 @@ void region_to_path(const Cairo::RefPtr &cr, const Cairo::RefPtr } } -// Update strategy +/* + * Update strategy + */ -// A class for controlling what order to update invalidated regions. +// A class hierarchy for controlling what order to update invalidated regions. class Updater { public: @@ -203,8 +209,8 @@ public: Updater(Cairo::RefPtr clean_region) : clean_region(std::move(clean_region)) {} - virtual void reset() {clean_region = Cairo::Region::create();} // Reset to clean region to empty. - virtual void intersect (const Geom::IntRect &rect) {clean_region->intersect(geom_to_cairo(rect));} // Called when the store changes position; clip everthing to the new store rect. + virtual void reset() {clean_region = Cairo::Region::create();} // Reset the clean region to empty. + virtual void intersect (const Geom::IntRect &rect) {clean_region->intersect(geom_to_cairo(rect));} // Called when the store changes position; clip everthing to the new store rectangle. virtual void mark_dirty(const Geom::IntRect &rect) {clean_region->subtract (geom_to_cairo(rect));} // Called on every invalidate event. virtual void mark_clean(const Geom::IntRect &rect) {clean_region->do_union (geom_to_cairo(rect));} // Called on every rectangle redrawn. @@ -396,7 +402,9 @@ make_updater(int type, Cairo::RefPtr clean_region = Cairo::Region } } -// Implementation class +/* + * Implementation class + */ class CanvasPrivate { @@ -406,13 +414,32 @@ public: Canvas *q; CanvasPrivate(Canvas *q) : q(q) {} - void add_idle(); - bool on_idle(); - void paint_rect_internal(Geom::IntRect const &rect); - void paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtr const &store); + // Preferences system + Prefs prefs; - std::optional old_bisector(const Geom::IntRect &rect); - std::optional new_bisector(const Geom::IntRect &rect); + // The updater; tracks the unclean region and decides how to redraw it. + std::unique_ptr updater; + + // Events system. Events that interact with the Canvas are placed into the event bucket until the start of the next frame. + std::vector> bucket; + bool pending_draw = false; + sigc::connection bucket_emptier; + int bucket_pos; + GdkEvent *ignore = nullptr; + + bool add_to_bucket(GdkEvent*); + void empty_bucket(); + bool process_bucketed_event(const GdkEvent&); + bool pick_current_item(const GdkEvent&); + bool emit_event(const GdkEvent&); + + // 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; // Important global properties of all the stores. If these change, all the stores must be recreated. int _device_scale = 1; @@ -423,9 +450,6 @@ public: Geom::Affine _store_affine; Cairo::RefPtr _backing_store, _outline_store; - // The updater. Holds the clean region, the subregion of the store with up-to-date content, and some additional state determining in what order the unclean regions should be redrawn. - std::unique_ptr updater; - // The snapshot store. Used to mask redraw delay on zoom/rotate. Geom::IntRect _snapshot_rect; Geom::Affine _snapshot_affine; @@ -438,83 +462,289 @@ public: bool solid_background; // Whether the last background set is solid. bool need_outline_store() const {return q->_split_mode != Inkscape::SplitMode::NORMAL || q->_render_mode == Inkscape::RenderMode::OUTLINE_OVERLAY;} - // Idle system callbacks. The high priority idle ensures at least one idle cycle between add_idle and on_draw. - sigc::connection hipri_idle; - sigc::connection lopri_idle; - bool on_hipri_idle(); - bool on_lopri_idle(); + // Drawing + bool on_idle(); + void paint_rect_internal(Geom::IntRect const &rect); + void paint_single_buffer(Geom::IntRect const &paint_rect, Cairo::RefPtr const &store); + std::optional old_bisector(const Geom::IntRect &rect); + std::optional new_bisector(const Geom::IntRect &rect); - // The event bucket. Events that arrive during a frame are put in the bucket, and the bucket is emptied immediately after the next on_draw, leaving the whole rest of the frame to proces the events. - struct GdkEventFreer {void operator()(GdkEvent *ev) const {gdk_event_free(ev);}}; - std::vector> bucket; - sigc::connection bucket_emptier; - int bucket_pos; + // Trivial overload of GtkWidget function. + void queue_draw_area(Geom::IntRect &rect); +}; - bool pending_bucket_emptier = false; - bool idle_running = false; +/* + * Events system + */ - void schedule_bucket_emptier(); - void empty_bucket(); +// 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. - GdkEvent *ignore = nullptr; +bool +Canvas::on_scroll_event(GdkEventScroll *scroll_event) +{ + return d->add_to_bucket(reinterpret_cast(scroll_event)); +} - // Useful overload of GtkWidget function. - void queue_draw_area(Geom::IntRect &rect); +bool +Canvas::on_button_press_event(GdkEventButton *button_event) +{ + return on_button_event(button_event); +} - // Preferences system - Prefs prefs; -}; +bool +Canvas::on_button_release_event(GdkEventButton *button_event) +{ + return on_button_event(button_event); +} -void CanvasPrivate::schedule_bucket_emptier() +// Unified handler for press and release events. +bool +Canvas::on_button_event(GdkEventButton *button_event) { - if (bucket_emptier.connected()) return; + // Sanity-check event type. + switch (button_event->type) { + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + case GDK_BUTTON_RELEASE: + break; // Good + default: + std::cerr << "Canvas::on_button_event: illegal event type!" << std::endl; + return false; + } - bucket_emptier = Glib::signal_idle().connect([this] { - bucket_emptier.disconnect(); - empty_bucket(); + // Drag the split view controller. + switch (button_event->type) { + case GDK_BUTTON_PRESS: + if (_hover_direction != Inkscape::SplitDirection::NONE) { + _split_dragging = true; + _split_drag_start = Geom::Point(button_event->x, button_event->y); + return true; + } + break; + case GDK_2BUTTON_PRESS: + if (_hover_direction != Inkscape::SplitDirection::NONE) { + _split_direction = _hover_direction; + _split_dragging = false; + queue_draw(); + return true; + } + break; + case GDK_BUTTON_RELEASE: + _split_dragging = false; + break; + } + + // Otherwise, handle as a delayed event. + return d->add_to_bucket(reinterpret_cast(button_event)); +} + +bool +Canvas::on_enter_notify_event(GdkEventCrossing *crossing_event) +{ + if (crossing_event->window != get_window()->gobj()) { + std::cout << " WHOOPS... this does really happen" << std::endl; return false; - }, G_PRIORITY_DEFAULT_IDLE - 5); // before lopri_idle + } + return d->add_to_bucket(reinterpret_cast(crossing_event)); } -void CanvasPrivate::empty_bucket() +bool +Canvas::on_leave_notify_event(GdkEventCrossing *crossing_event) { - framecheck_whole_function(this) + if (crossing_event->window != get_window()->gobj()) { + std::cout << " WHOOPS... this does really happen" << std::endl; + return false; + } + return d->add_to_bucket(reinterpret_cast(crossing_event)); +} - pending_bucket_emptier = false; +bool +Canvas::on_focus_in_event(GdkEventFocus *focus_event) +{ + grab_focus(); + return false; +} - bucket_pos = 0; +bool +Canvas::on_key_press_event(GdkEventKey *key_event) +{ + return d->add_to_bucket(reinterpret_cast(key_event)); +} - while (bucket_pos < bucket.size()) { - // Extract next event. - auto event = std::move(bucket[bucket_pos]); - bucket_pos++; +bool +Canvas::on_key_release_event(GdkEventKey *key_event) +{ + return d->add_to_bucket(reinterpret_cast(key_event)); +} + +bool +Canvas::on_motion_notify_event(GdkEventMotion *motion_event) +{ + // Handle interactions with the split view controller. + if (_desktop) { + Geom::IntPoint cursor_position = Geom::IntPoint(motion_event->x, motion_event->y); + + // Check if we are near the edge. If so, revert to normal mode. + if (_split_mode == Inkscape::SplitMode::SPLIT && _split_dragging) { + 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_mode = Inkscape::SplitMode::NORMAL; + _split_position = Geom::Point(-1, -1); + _hover_direction = Inkscape::SplitDirection::NONE; + set_cursor(); + queue_draw(); + + // Update action (turn into utility function?). + auto window = dynamic_cast(get_toplevel()); + if (!window) { + std::cerr << "Canvas::on_motion_notify_event: window missing!" << std::endl; + return true; + } + + auto action = window->lookup_action("canvas-split-mode"); + if (!action) { + std::cerr << "Canvas::on_motion_notify_event: action 'canvas-split-mode' missing!" << std::endl; + return true; + } + + auto saction = Glib::RefPtr::cast_dynamic(action); + if (!saction) { + std::cerr << "Canvas::on_motion_notify_event: action 'canvas-split-mode' not SimpleAction!" << std::endl; + return true; + } + + saction->change_state((int)Inkscape::SplitMode::NORMAL); + + return true; + } + } - // Block undo/redo while anything is dragged. - if (event->type == GDK_BUTTON_PRESS && event->button.button == 1) { - q->_is_dragging = true; - } else if (event->type == GDK_BUTTON_RELEASE) { - q->_is_dragging = false; + if (_split_mode == Inkscape::SplitMode::XRAY) { + _split_position = cursor_position; + queue_draw(); } - bool finished = false; + if (_split_mode == Inkscape::SplitMode::SPLIT) { + Inkscape::SplitDirection hover_direction = Inkscape::SplitDirection::NONE; + Geom::Point difference(cursor_position - _split_position); + + // Move controller + if (_split_dragging) { + Geom::Point delta = cursor_position - _split_drag_start; // We don't use _split_position + if (_hover_direction == Inkscape::SplitDirection::HORIZONTAL) { + _split_position += Geom::Point(0, delta.y()); + } else if (_hover_direction == Inkscape::SplitDirection::VERTICAL) { + _split_position += Geom::Point(delta.x(), 0); + } else { + _split_position += delta; + } + _split_drag_start = cursor_position; + queue_draw(); + return true; + } - if (q->_current_canvas_item) { - // Choose where to send event. - CanvasItem *item = q->_current_canvas_item; + if (Geom::distance(cursor_position, _split_position) < 20 * d->_device_scale) { + // We're hovering over circle, figure out which direction we are in. + if (difference.y() - difference.x() > 0) { + if (difference.y() + difference.x() > 0) { + hover_direction = Inkscape::SplitDirection::SOUTH; + } else { + hover_direction = Inkscape::SplitDirection::WEST; + } + } else { + if (difference.y() + difference.x() > 0) { + hover_direction = Inkscape::SplitDirection::EAST; + } else { + hover_direction = Inkscape::SplitDirection::NORTH; + } + } + } else if (_split_direction == Inkscape::SplitDirection::NORTH || + _split_direction == Inkscape::SplitDirection::SOUTH) { + if (std::abs(difference.y()) < 3 * d->_device_scale) { + // We're hovering over horizontal line + hover_direction = Inkscape::SplitDirection::HORIZONTAL; + } + } else { + if (std::abs(difference.x()) < 3 * d->_device_scale) { + // We're hovering over vertical line + hover_direction = Inkscape::SplitDirection::VERTICAL; + } + } - if (q->_grabbed_canvas_item && !q->_current_canvas_item->is_descendant_of(q->_grabbed_canvas_item)) { - item = q->_grabbed_canvas_item; + if (_hover_direction != hover_direction) { + _hover_direction = hover_direction; + set_cursor(); + queue_draw(); } - // Propagate the event up the canvas item hierarchy until handled. - while (item) { - finished = item->handle_event(event.get()); - if (finished) break; - item = item->get_parent(); + if (_hover_direction != Inkscape::SplitDirection::NONE) { + // We're hovering, don't pick or emit event. + return true; } } + } // if (_desktop) + + // Otherwise, handle as a delayed event. + return d->add_to_bucket(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(GdkEvent *event) +{ + framecheck_whole_function(this) + + // Prevent re-fired events from going through again. + if (event == ignore) { + return false; + } + + // If this is the first event, make sure the bucket will be emptied in the near future. + if (bucket.empty() && !pending_draw) { + q->add_tick_callback([this] (const Glib::RefPtr&) { + empty_bucket(); + return false; + }); + } + + // Add a copy to the queue. + bucket.emplace_back(std::make_unique(*event)); + + // Tell GTK the event was handled. + return true; +} + +// The following functions run at the start of the next frame. + +// Process bucketed events. +void +CanvasPrivate::empty_bucket() +{ + framecheck_whole_function(this) + + // Initialise iteration index; may be incremented externally by gobblers. + bucket_pos = 0; - if (!finished) { + while (bucket_pos < bucket.size()) { + // Extract next event. + auto event = std::move(bucket[bucket_pos]); + bucket_pos++; + + // Process the event and see if it was handled. + bool handled = process_bucketed_event(*event); + + if (!handled) { // Re-fire the event at the window, and ignore it when it comes back here again. ignore = event.get(); q->get_toplevel()->event(event.get()); @@ -525,15 +755,16 @@ void CanvasPrivate::empty_bucket() bucket.clear(); } -// Used by some tools to batch backlogs of key events that may have built up after a freeze. Called during 'empty_bucket'. -int Canvas::gobble_key_events(guint keyval, guint mask) const +// Called during 'empty_bucket' 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) const { int count = 0; while (d->bucket_pos < d->bucket.size()) { auto &event = d->bucket[d->bucket_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 processing. + // Discard event and continue. if (event->type == GDK_KEY_PRESS) count++; d->bucket_pos++; } @@ -543,20 +774,21 @@ int Canvas::gobble_key_events(guint keyval, guint mask) const } } - if (count > 0 && d->prefs.debug_logging) std::cout << "Gobbled " << count << " key presses" << std::endl; + if (count > 0 && d->prefs.debug_logging) std::cout << "Gobbled " << count << " key press(es)" << std::endl; return count; } -// Used by some tools to ignore backlogs of motion events that may have built up after a freeze. Called during 'empty_bucket'. -void Canvas::gobble_motion_events(guint mask) const +// Called during 'empty_bucket' by some tools to ignore backlogs of motion events that may have built up after a freeze. +void +Canvas::gobble_motion_events(guint mask) const { int count = 0; while (d->bucket_pos < d->bucket.size()) { auto &event = d->bucket[d->bucket_pos]; if (event->type == GDK_MOTION_NOTIFY && (event->motion.state & mask)) { - // Discard event and continue processing. + // Discard event and continue. count++; d->bucket_pos++; } @@ -566,62 +798,407 @@ void Canvas::gobble_motion_events(guint mask) const } } - if (count > 0 && d->prefs.debug_logging) std::cout << "Gobbled " << count << " motion events" << std::endl; -} - -void CanvasPrivate::queue_draw_area(Geom::IntRect &rect) -{ - q->queue_draw_area(rect.left(), rect.top(), rect.width(), rect.height()); + if (count > 0 && d->prefs.debug_logging) std::cout << "Gobbled " << count << " motion event(s)" << std::endl; } -Canvas::Canvas() - : d(std::make_unique(this)) -{ - set_name("InkscapeCanvas"); +// 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. - // Events - add_events(Gdk::BUTTON_PRESS_MASK | - Gdk::BUTTON_RELEASE_MASK | - Gdk::ENTER_NOTIFY_MASK | - Gdk::LEAVE_NOTIFY_MASK | - Gdk::FOCUS_CHANGE_MASK | - Gdk::KEY_PRESS_MASK | - Gdk::KEY_RELEASE_MASK | - Gdk::POINTER_MOTION_MASK | - Gdk::SCROLL_MASK | - Gdk::SMOOTH_SCROLL_MASK ); +bool +CanvasPrivate::process_bucketed_event(const GdkEvent &event) +{ + auto calc_button_mask = [&] () -> int { + switch (event.button.button) { + case 1: return GDK_BUTTON1_MASK; break; // Fixme: These all used to be GDK_BUTTON1_MASK! Was that intentional? I changed it just in case. Revert on breakage. + case 2: return GDK_BUTTON2_MASK; break; + case 3: return GDK_BUTTON3_MASK; break; + case 4: return GDK_BUTTON4_MASK; break; + case 5: return GDK_BUTTON5_MASK; break; + default: return 0; // Buttons can range at least to 9 but mask defined only to 5. + } + }; - // Preferences - d->prefs.grabsize.action = [=] {_canvas_item_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.update_strategy.action = [=] {d->updater = make_updater(d->prefs.update_strategy, std::move(d->updater->clean_region));}; - d->prefs.outline_overlay_opacity.action = [=] {queue_draw();}; + // Do event-specific processing. + switch (event.type) { - // Give _pick_event an initial definition. - _pick_event.type = GDK_LEAVE_NOTIFY; - _pick_event.crossing.x = 0; - _pick_event.crossing.y = 0; + case GDK_SCROLL: + return emit_event(event); - // Drawing - d->updater = make_updater(d->prefs.update_strategy); + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + { + // Pick the current item as if the button were not pressed... + q->_state = event.button.state; + pick_current_item(event); - _background = Cairo::SolidPattern::create_rgb(1.0, 1.0, 1.0); - d->solid_background = true; + // ...then process the event. + q->_state ^= calc_button_mask(); + bool retval = emit_event(event); - _canvas_item_root = new Inkscape::CanvasItemGroup(nullptr); - _canvas_item_root->set_name("CanvasItemGroup:Root"); - _canvas_item_root->set_canvas(this); -} + return retval; + } -Canvas::~Canvas() -{ - assert(!_desktop); + case GDK_BUTTON_RELEASE: + { + // Process the event as if the button were pressed... + q->_state = event.button.state; + bool retval = emit_event(event); - _drawing = nullptr; - _in_destruction = true; + // ...then repick after the button has been released. + auto event_copy = event; + event_copy.button.state ^= calc_button_mask(); + q->_state = event_copy.button.state; + pick_current_item(event_copy); + + return retval; + } + + case GDK_ENTER_NOTIFY: + q->_state = event.crossing.state; + return pick_current_item(event); + + case GDK_LEAVE_NOTIFY: + q->_state = event.crossing.state; + // This is needed to remove alignment or distribution snap indicators. + if (q->_desktop) { + q->_desktop->snapindicator->remove_snaptarget(); + } + return pick_current_item(event); + + case GDK_KEY_PRESS: + case GDK_KEY_RELEASE: + return emit_event(event); + + case GDK_MOTION_NOTIFY: + q->_state = event.motion.state; + pick_current_item(event); + return emit_event(event); + + default: + return false; + } +} + +// This function is called by 'process_bucketed_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 +// 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. +// +// Canvas items register their interest by connecting to the "event" signal. +// Example in desktop.cpp: +// canvas_catchall->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), this)); +bool +CanvasPrivate::pick_current_item(const GdkEvent &event) +{ + // Ensure geometry is correct. + auto affine = decoupled_mode ? _store_affine : q->_affine; + if (q->_need_update || geom_affine != affine) { + q->_canvas_item_root->update(affine); + geom_affine = affine; + q->_need_update = false; + } + + int button_down = 0; + if (!q->_all_enter_events) { + // Only set true in connector-tool.cpp. + + // If a button is down, we'll perform enter and leave events on the + // current item, but not enter on any other item. This is more or + // less like X pointer grabbing for canvas items. + button_down = q->_state & (GDK_BUTTON1_MASK | + GDK_BUTTON2_MASK | + GDK_BUTTON3_MASK | + GDK_BUTTON4_MASK | + GDK_BUTTON5_MASK); + if (!button_down) q->_left_grabbed_item = false; + } + + // Save the event in the canvas. This is used to synthesize enter and + // leave events in case the current item changes. It is also used to + // re-pick the current item if the current one gets deleted. Also, + // synthesize an enter event. + if (&event != &q->_pick_event) { + if (event.type == GDK_MOTION_NOTIFY || event.type == GDK_BUTTON_RELEASE) { + // Convert to GDK_ENTER_NOTIFY + + // These fields have the same offsets in both types of events. + q->_pick_event.crossing.type = GDK_ENTER_NOTIFY; + q->_pick_event.crossing.window = event.motion.window; + q->_pick_event.crossing.send_event = event.motion.send_event; + q->_pick_event.crossing.subwindow = nullptr; + q->_pick_event.crossing.x = event.motion.x; + q->_pick_event.crossing.y = event.motion.y; + q->_pick_event.crossing.mode = GDK_CROSSING_NORMAL; + q->_pick_event.crossing.detail = GDK_NOTIFY_NONLINEAR; + q->_pick_event.crossing.focus = false; + q->_pick_event.crossing.state = event.motion.state; + + // These fields don't have the same offsets in both types of events. + if (event.type == GDK_MOTION_NOTIFY) { + q->_pick_event.crossing.x_root = event.motion.x_root; + q->_pick_event.crossing.y_root = event.motion.y_root; + } else { + q->_pick_event.crossing.x_root = event.button.x_root; + q->_pick_event.crossing.y_root = event.button.y_root; + } + + } else { + q->_pick_event = event; + } + } + + if (q->_in_repick) { + // Don't do anything else if this is a recursive call. + return false; + } + + // Find new item + q->_current_canvas_item_new = nullptr; + + if (q->_pick_event.type != GDK_LEAVE_NOTIFY && q->_canvas_item_root->is_visible()) { + // Leave notify means there is no current item. + // Find closest item. + double x = 0.0; + double y = 0.0; + + if (q->_pick_event.type == GDK_ENTER_NOTIFY) { + x = q->_pick_event.crossing.x; + y = q->_pick_event.crossing.y; + } else { + x = q->_pick_event.motion.x; + y = q->_pick_event.motion.y; + } + + // If in split mode, look at where cursor is to see if one should pick with outline mode. + q->_drawing->setRenderMode(q->_render_mode); + if (q->_split_mode == Inkscape::SplitMode::SPLIT && q->_render_mode != Inkscape::RenderMode::OUTLINE_OVERLAY) { + if ((q->_split_direction == Inkscape::SplitDirection::NORTH && y > q->_split_position.y()) || + (q->_split_direction == Inkscape::SplitDirection::SOUTH && y < q->_split_position.y()) || + (q->_split_direction == Inkscape::SplitDirection::WEST && x > q->_split_position.x()) || + (q->_split_direction == Inkscape::SplitDirection::EAST && x < q->_split_position.x()) ) { + q->_drawing->setRenderMode(Inkscape::RenderMode::OUTLINE); + } + } + // Convert to world coordinates. + auto p = Geom::Point(x, y) + q->_pos; + if (decoupled_mode) { + p *= _store_affine * q->_affine.inverse(); + } + + q->_current_canvas_item_new = q->_canvas_item_root->pick_item(p); + // if (q->_current_canvas_item_new) { + // std::cout << " PICKING: FOUND ITEM: " << q->_current_canvas_item_new->get_name() << std::endl; + // } else { + // std::cout << " PICKING: DID NOT FIND ITEM" << std::endl; + // } + } + + if (q->_current_canvas_item_new == q->_current_canvas_item && !q->_left_grabbed_item) { + // Current item did not change! + return false; + } + + // Synthesize events for old and new current items. + bool retval = false; + if (q->_current_canvas_item_new != q->_current_canvas_item && + q->_current_canvas_item != nullptr && + !q->_left_grabbed_item ) { + + GdkEvent new_event; + new_event = q->_pick_event; + new_event.type = GDK_LEAVE_NOTIFY; + new_event.crossing.detail = GDK_NOTIFY_ANCESTOR; + new_event.crossing.subwindow = nullptr; + q->_in_repick = true; + retval = emit_event(new_event); + q->_in_repick = false; + } + + if (q->_all_enter_events == false) { + // new_current_item may have been set to nullptr during the call to emitEvent() above. + if (q->_current_canvas_item_new != q->_current_canvas_item && button_down) { + q->_left_grabbed_item = true; + return retval; + } + } + + // Handle the rest of cases + q->_left_grabbed_item = false; + q->_current_canvas_item = q->_current_canvas_item_new; + + if (q->_current_canvas_item != nullptr) { + GdkEvent new_event; + new_event = q->_pick_event; + new_event.type = GDK_ENTER_NOTIFY; + new_event.crossing.detail = GDK_NOTIFY_ANCESTOR; + new_event.crossing.subwindow = nullptr; + retval = emit_event(new_event); + } + + return retval; +} + +// Fires an event at the canvas, after a little pre-processing. Returns true if handled. +bool +CanvasPrivate::emit_event(const GdkEvent &event) +{ + // Handle grabbed items. + if (q->_grabbed_canvas_item) { + auto mask = (Gdk::EventMask)0; + + switch (event.type) { + case GDK_ENTER_NOTIFY: + mask = Gdk::ENTER_NOTIFY_MASK; + break; + case GDK_LEAVE_NOTIFY: + mask = Gdk::LEAVE_NOTIFY_MASK; + break; + case GDK_MOTION_NOTIFY: + mask = Gdk::POINTER_MOTION_MASK; + break; + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + mask = Gdk::BUTTON_PRESS_MASK; + break; + case GDK_BUTTON_RELEASE: + mask = Gdk::BUTTON_RELEASE_MASK; + break; + case GDK_KEY_PRESS: + mask = Gdk::KEY_PRESS_MASK; + break; + case GDK_KEY_RELEASE: + mask = Gdk::KEY_RELEASE_MASK; + break; + case GDK_SCROLL: + mask = Gdk::SCROLL_MASK; + mask |= Gdk::SMOOTH_SCROLL_MASK; + break; + default: + break; + } + + if (!(mask & q->_grabbed_event_mask)) { + return false; + } + } + + // Convert to world coordinates. We have two different cases due to different event structures. + auto conv = [&, this] (double &x, double &y) { + auto p = Geom::Point(x, y) + q->_pos; + if (decoupled_mode) { + p *= _store_affine * q->_affine.inverse(); + } + x = p.x(); + y = p.y(); + }; + + auto event_copy = event; + switch (event.type) { + case GDK_ENTER_NOTIFY: + case GDK_LEAVE_NOTIFY: + conv(event_copy.crossing.x, event_copy.crossing.y); + break; + case GDK_MOTION_NOTIFY: + case GDK_BUTTON_PRESS: + case GDK_2BUTTON_PRESS: + case GDK_3BUTTON_PRESS: + case GDK_BUTTON_RELEASE: + conv(event_copy.motion.x, event_copy.motion.y); + break; + default: + break; + } + + // Block undo/redo while anything is dragged. + if (event.type == GDK_BUTTON_PRESS && event.button.button == 1) { + q->_is_dragging = true; + } else if (event.type == GDK_BUTTON_RELEASE) { + q->_is_dragging = false; + } + + if (q->_current_canvas_item) { + // Choose where to send event. + auto item = q->_current_canvas_item; + + if (q->_grabbed_canvas_item && !q->_current_canvas_item->is_descendant_of(q->_grabbed_canvas_item)) { + item = q->_grabbed_canvas_item; + } + + // Propagate the event up the canvas item hierarchy until handled. + while (item) { + if (item->handle_event(&event_copy)) return true; + item = item->get_parent(); + } + } + + return false; +} + +/* + * The Rest + */ + +void CanvasPrivate::queue_draw_area(Geom::IntRect &rect) +{ + q->queue_draw_area(rect.left(), rect.top(), rect.width(), rect.height()); +} + +Canvas::Canvas() + : d(std::make_unique(this)) +{ + set_name("InkscapeCanvas"); + + // Events + add_events(Gdk::BUTTON_PRESS_MASK | + Gdk::BUTTON_RELEASE_MASK | + Gdk::ENTER_NOTIFY_MASK | + Gdk::LEAVE_NOTIFY_MASK | + Gdk::FOCUS_CHANGE_MASK | + Gdk::KEY_PRESS_MASK | + Gdk::KEY_RELEASE_MASK | + Gdk::POINTER_MOTION_MASK | + Gdk::SCROLL_MASK | + Gdk::SMOOTH_SCROLL_MASK ); + + // Preferences + d->prefs.grabsize.action = [=] {_canvas_item_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.update_strategy.action = [=] {d->updater = make_updater(d->prefs.update_strategy, std::move(d->updater->clean_region));}; + d->prefs.outline_overlay_opacity.action = [=] {queue_draw();}; + + // Give _pick_event an initial definition. + _pick_event.type = GDK_LEAVE_NOTIFY; + _pick_event.crossing.x = 0; + _pick_event.crossing.y = 0; + + // Drawing + d->updater = make_updater(d->prefs.update_strategy); + + _background = Cairo::SolidPattern::create_rgb(1.0, 1.0, 1.0); + d->solid_background = true; + + _canvas_item_root = new Inkscape::CanvasItemGroup(nullptr); + _canvas_item_root->set_name("CanvasItemGroup:Root"); + _canvas_item_root->set_canvas(this); +} + +Canvas::~Canvas() +{ + assert(!_desktop); + + _drawing = nullptr; + _in_destruction = true; // Disconnect signals. Otherwise called after destructor and crashes. d->hipri_idle.disconnect(); @@ -829,340 +1406,82 @@ Canvas::set_render_mode(Inkscape::RenderMode mode) _render_mode = mode; redraw_all(); } - if (_desktop) { - _desktop->setWindowTitle(); // Mode is listed in title. - } -} - -void -Canvas::set_color_mode(Inkscape::ColorMode mode) -{ - if (_color_mode != mode) { - _color_mode = mode; - redraw_all(); - } - if (_desktop) { - _desktop->setWindowTitle(); // Mode is listed in title. - } -} - -void -Canvas::set_split_mode(Inkscape::SplitMode mode) -{ - if (_split_mode != mode) { - _split_mode = mode; - redraw_all(); - } -} - -void -Canvas::set_split_direction(Inkscape::SplitDirection dir) -{ - if (_split_direction != dir) { - _split_direction = dir; - redraw_all(); - } -} - -Cairo::RefPtr Canvas::get_backing_store() const -{ - return d->_backing_store; -} - -/** - * Clear current and grabbed items. - */ -void -Canvas::canvas_item_clear(Inkscape::CanvasItem* item) -{ - if (item == _current_canvas_item) { - _current_canvas_item = nullptr; - _need_repick = true; - } - - if (item == _current_canvas_item_new) { - _current_canvas_item_new = nullptr; - _need_repick = true; - } - - if (item == _grabbed_canvas_item) { - _grabbed_canvas_item = nullptr; - auto const display = Gdk::Display::get_default(); - auto const seat = display->get_default_seat(); - seat->ungrab(); - } -} - -// ============== Protected Functions ============== - -void -Canvas::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const -{ - minimum_width = natural_width = 256; -} - -void -Canvas::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const -{ - minimum_height = natural_height = 256; -} - -// ******* Event handlers ****** -bool -Canvas::on_scroll_event(GdkEventScroll *scroll_event) -{ - // Scroll canvas and in Select Tool, cycle selection through objects under cursor. - return emit_event(reinterpret_cast(scroll_event)); -} - -// Our own function that combines press and release. -bool -Canvas::on_button_event(GdkEventButton *button_event) -{ - // Dispatch normally regardless of the event's window if an item - // has a pointer grab in effect. - auto window = get_window(); - if (!_grabbed_canvas_item && window->gobj() != button_event->window) { - return false; - } - - int mask = 0; - switch (button_event->button) { - case 1: mask = GDK_BUTTON1_MASK; break; - case 2: mask = GDK_BUTTON1_MASK; break; - case 3: mask = GDK_BUTTON1_MASK; break; - case 4: mask = GDK_BUTTON1_MASK; break; - case 5: mask = GDK_BUTTON1_MASK; break; - default: mask = 0; // Buttons can range at least to 9 but mask defined only to 5. - } - - bool retval = false; - switch (button_event->type) { - case GDK_BUTTON_PRESS: - - if (_hover_direction != Inkscape::SplitDirection::NONE) { - // We're hovering over Split controller. - _split_dragging = true; - _split_drag_start = Geom::Point(button_event->x, button_event->y); - break; - } - // Fallthrough - - case GDK_2BUTTON_PRESS: - - if (_hover_direction != Inkscape::SplitDirection::NONE) { - _split_direction = _hover_direction; - _split_dragging = false; - queue_draw(); - break; - } - // Fallthrough - - case GDK_3BUTTON_PRESS: - // Pick the current item as if the button were not pressed and then process event. - - _state = button_event->state; - pick_current_item(reinterpret_cast(button_event)); - _state ^= mask; - retval = emit_event(reinterpret_cast(button_event)); - break; - - case GDK_BUTTON_RELEASE: - // Process the event as if the button were pressed, then repick after the button has - // been released. - _split_dragging = false; - - _state = button_event->state; - retval = emit_event(reinterpret_cast(button_event)); - button_event->state ^= mask; - _state = button_event->state; - pick_current_item(reinterpret_cast(button_event)); - button_event->state ^= mask; - break; - - default: - std::cerr << "Canvas::on_button_event: illegal event type!" << std::endl; - } - - return retval; -} - -bool -Canvas::on_button_press_event(GdkEventButton *button_event) -{ - return on_button_event(button_event); -} - -bool -Canvas::on_button_release_event(GdkEventButton *button_event) -{ - return on_button_event(button_event); -} - -bool -Canvas::on_enter_notify_event(GdkEventCrossing *crossing_event) -{ - auto window = get_window(); - if (window->gobj() != crossing_event->window) { - std::cout << " WHOOPS... this does really happen" << std::endl; - return false; - } - _state = crossing_event->state; - return pick_current_item(reinterpret_cast(crossing_event)); + if (_desktop) { + _desktop->setWindowTitle(); // Mode is listed in title. + } } -bool -Canvas::on_leave_notify_event(GdkEventCrossing *crossing_event) +void +Canvas::set_color_mode(Inkscape::ColorMode mode) { - auto window = get_window(); - if (window->gobj() != crossing_event->window) { - std::cout << " WHOOPS... this does really happen" << std::endl; - return false; + if (_color_mode != mode) { + _color_mode = mode; + redraw_all(); + } + if (_desktop) { + _desktop->setWindowTitle(); // Mode is listed in title. } - _state = crossing_event->state; - // this is needed to remove alignment or distribution snap indicators - if (_desktop) - _desktop->snapindicator->remove_snaptarget(); - return pick_current_item(reinterpret_cast(crossing_event)); } -bool -Canvas::on_focus_in_event(GdkEventFocus *focus_event) +void +Canvas::set_split_mode(Inkscape::SplitMode mode) { - grab_focus(); - return false; + if (_split_mode != mode) { + _split_mode = mode; + redraw_all(); + } } -// Actually, key events never reach here. -bool -Canvas::on_key_press_event(GdkEventKey *key_event) +void +Canvas::set_split_direction(Inkscape::SplitDirection dir) { - return emit_event(reinterpret_cast(key_event)); + if (_split_direction != dir) { + _split_direction = dir; + redraw_all(); + } } -// Actually, key events never reach here. -bool -Canvas::on_key_release_event(GdkEventKey *key_event) +Cairo::RefPtr Canvas::get_backing_store() const { - return emit_event(reinterpret_cast(key_event)); + return d->_backing_store; } -bool -Canvas::on_motion_notify_event(GdkEventMotion *motion_event) +/** + * Clear current and grabbed items. + */ +void +Canvas::canvas_item_clear(Inkscape::CanvasItem* item) { - Geom::IntPoint cursor_position = Geom::IntPoint(motion_event->x, motion_event->y); - - if (_desktop) { - // Check if we are near the edge. If so, revert to normal mode. - if (_split_mode == Inkscape::SplitMode::SPLIT && _split_dragging) { - 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_mode = Inkscape::SplitMode::NORMAL; - _split_position = Geom::Point(-1, -1); - _hover_direction = Inkscape::SplitDirection::NONE; - set_cursor(); - queue_draw(); - - // Update action (turn into utility function?). - auto window = dynamic_cast(get_toplevel()); - if (!window) { - std::cerr << "Canvas::on_motion_notify_event: window missing!" << std::endl; - return true; - } - - auto action = window->lookup_action("canvas-split-mode"); - if (!action) { - std::cerr << "Canvas::on_motion_notify_event: action 'canvas-split-mode' missing!" << std::endl; - return true; - } - - auto saction = Glib::RefPtr::cast_dynamic(action); - if (!saction) { - std::cerr << "Canvas::on_motion_notify_event: action 'canvas-split-mode' not SimpleAction!" << std::endl; - return true; - } - - saction->change_state((int)Inkscape::SplitMode::NORMAL); - - return true; - } + if (item == _current_canvas_item) { + _current_canvas_item = nullptr; + _need_repick = true; } - if (_split_mode == Inkscape::SplitMode::XRAY) { - _split_position = cursor_position; - queue_draw(); // Re-blit + if (item == _current_canvas_item_new) { + _current_canvas_item_new = nullptr; + _need_repick = true; } - if (_split_mode == Inkscape::SplitMode::SPLIT) { - - Inkscape::SplitDirection hover_direction = Inkscape::SplitDirection::NONE; - Geom::Point difference(cursor_position - _split_position); - - // Move controller - if (_split_dragging) { - Geom::Point delta = cursor_position - _split_drag_start; // We don't use _split_position - if (_hover_direction == Inkscape::SplitDirection::HORIZONTAL) { - _split_position += Geom::Point(0, delta.y()); - } else if (_hover_direction == Inkscape::SplitDirection::VERTICAL) { - _split_position += Geom::Point(delta.x(), 0); - } else { - _split_position += delta; - } - _split_drag_start = cursor_position; - queue_draw(); - return true; - } - - if (Geom::distance(cursor_position, _split_position) < 20 * d->_device_scale) { - - // We're hovering over circle, figure out which direction we are in. - if (difference.y() - difference.x() > 0) { - if (difference.y() + difference.x() > 0) { - hover_direction = Inkscape::SplitDirection::SOUTH; - } else { - hover_direction = Inkscape::SplitDirection::WEST; - } - } else { - if (difference.y() + difference.x() > 0) { - hover_direction = Inkscape::SplitDirection::EAST; - } else { - hover_direction = Inkscape::SplitDirection::NORTH; - } - } - } else if (_split_direction == Inkscape::SplitDirection::NORTH || - _split_direction == Inkscape::SplitDirection::SOUTH) { - if (std::abs(difference.y()) < 3 * d->_device_scale) { - // We're hovering over horizontal line - hover_direction = Inkscape::SplitDirection::HORIZONTAL; - } - } else { - if (std::abs(difference.x()) < 3 * d->_device_scale) { - // We're hovering over vertical line - hover_direction = Inkscape::SplitDirection::VERTICAL; - } - } + if (item == _grabbed_canvas_item) { + _grabbed_canvas_item = nullptr; + auto const display = Gdk::Display::get_default(); + auto const seat = display->get_default_seat(); + seat->ungrab(); + } +} - if (_hover_direction != hover_direction) { - _hover_direction = hover_direction; - set_cursor(); - queue_draw(); - } +// ============== Protected Functions ============== - if (_hover_direction != Inkscape::SplitDirection::NONE) { - // We're hovering, don't pick or emit event. - return true; - } - } - } // End if(desktop) +void +Canvas::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const +{ + minimum_width = natural_width = 256; +} - _state = motion_event->state; - pick_current_item(reinterpret_cast(motion_event)); - bool status = emit_event(reinterpret_cast(motion_event)); - return status; +void +Canvas::get_preferred_height_vfunc(int &minimum_height, int &natural_height) const +{ + minimum_height = natural_height = 256; } /** @@ -1411,15 +1730,19 @@ Canvas::on_draw(const Cairo::RefPtr<::Cairo::Context> &cr) cr->restore(); } - // Process bucketed events as soon as possible after draw. - if (d->pending_bucket_emptier) { - if (!d->bucket.empty()) { - d->schedule_bucket_emptier(); - } else { - d->pending_bucket_emptier = false; - } + // 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 that runs as soon as this function is completed. + if (!d->bucket.empty()) { + Glib::signal_idle().connect([this] { + d->empty_bucket(); + return false; + }, G_PRIORITY_DEFAULT_IDLE - 5); // before lopri_idle } + // 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->frame(); @@ -1469,7 +1792,7 @@ calc_affine_diff(const Geom::Affine &a, const Geom::Affine &b) { auto coarsen(const Cairo::RefPtr ®ion, int min_size, int glue_size, double min_fullness) { - // Sort the rects by minExtent + // Sort the rects by minExtent. struct Compare { bool operator()(const Geom::IntRect &a, const Geom::IntRect &b) const { @@ -1496,7 +1819,7 @@ coarsen(const Cairo::RefPtr ®ion, int min_size, int glue_size, auto rect = *rects.begin(); rects.erase(rects.begin()); - // Initialise the effective glue size + // Initialise the effective glue size. int effective_glue_size = glue_size; while (true) { @@ -2030,7 +2353,8 @@ CanvasPrivate::paint_rect_internal(Geom::IntRect const &rect) // Schedule repaint queue_draw_area(repaint_rect); // Guarantees on_draw will be called in the future. - pending_bucket_emptier = true; // On the next call to on_draw, schedules the bucket emptier. + bucket_emptier.disconnect(); + pending_draw = true; } else { // Get rectangle needing repaint (transform into screen space, take bounding box, round outwards) auto pl = Geom::Parallelogram(rect); @@ -2044,7 +2368,8 @@ CanvasPrivate::paint_rect_internal(Geom::IntRect const &rect) if (repaint_rect & screen_rect) { // Schedule repaint queue_draw_area(repaint_rect); - pending_bucket_emptier = true; + bucket_emptier.disconnect(); + pending_draw = true; } } } @@ -2208,246 +2533,6 @@ Canvas::set_cursor() { } } - -// This routine reacts to events from the canvas. It's 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. -// -// Canvas items register their interest by connecting to the "event" signal. -// Example in desktop.cpp: -// canvas_catchall->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), this)); -bool -Canvas::pick_current_item(GdkEvent *event) -{ - // Ensure geometry is correct. - auto affine = d->decoupled_mode ? d->_store_affine : _affine; - if (_need_update || d->geom_affine != affine) { - _canvas_item_root->update(affine); - d->geom_affine = affine; - _need_update = false; - } - - int button_down = 0; - if (_all_enter_events == false) { - // Only set true in connector-tool.cpp. - - // If a button is down, we'll perform enter and leave events on the - // current item, but not enter on any other item. This is more or - // less like X pointer grabbing for canvas items. - button_down = _state & (GDK_BUTTON1_MASK | - GDK_BUTTON2_MASK | - GDK_BUTTON3_MASK | - GDK_BUTTON4_MASK | - GDK_BUTTON5_MASK); - if (!button_down) _left_grabbed_item = false; - } - - // Save the event in the canvas. This is used to synthesize enter and - // leave events in case the current item changes. It is also used to - // re-pick the current item if the current one gets deleted. Also, - // synthesize an enter event. - if (event != &_pick_event) { - if (event->type == GDK_MOTION_NOTIFY || event->type == GDK_BUTTON_RELEASE) { - // Convert to GDK_ENTER_NOTIFY - - // These fields have the same offsets in both types of events. - _pick_event.crossing.type = GDK_ENTER_NOTIFY; - _pick_event.crossing.window = event->motion.window; - _pick_event.crossing.send_event = event->motion.send_event; - _pick_event.crossing.subwindow = nullptr; - _pick_event.crossing.x = event->motion.x; - _pick_event.crossing.y = event->motion.y; - _pick_event.crossing.mode = GDK_CROSSING_NORMAL; - _pick_event.crossing.detail = GDK_NOTIFY_NONLINEAR; - _pick_event.crossing.focus = false; - _pick_event.crossing.state = event->motion.state; - - // These fields don't have the same offsets in both types of events. - if (event->type == GDK_MOTION_NOTIFY) { - _pick_event.crossing.x_root = event->motion.x_root; - _pick_event.crossing.y_root = event->motion.y_root; - } else { - _pick_event.crossing.x_root = event->button.x_root; - _pick_event.crossing.y_root = event->button.y_root; - } - } else { - _pick_event = *event; - } - } - - if (_in_repick) { - // Don't do anything else if this is a recursive call. - return false; - } - - // Find new item - _current_canvas_item_new = nullptr; - - if (_pick_event.type != GDK_LEAVE_NOTIFY && _canvas_item_root->is_visible()) { - // Leave notify means there is no current item. - // Find closest item. - double x = 0.0; - double y = 0.0; - - if (_pick_event.type == GDK_ENTER_NOTIFY) { - x = _pick_event.crossing.x; - y = _pick_event.crossing.y; - } else { - x = _pick_event.motion.x; - y = _pick_event.motion.y; - } - - // If in split mode, look at where cursor is to see if one should pick with outline mode. - _drawing->setRenderMode(_render_mode); - if (_split_mode == Inkscape::SplitMode::SPLIT && _render_mode != Inkscape::RenderMode::OUTLINE_OVERLAY) { - if ((_split_direction == Inkscape::SplitDirection::NORTH && y > _split_position.y()) || - (_split_direction == Inkscape::SplitDirection::SOUTH && y < _split_position.y()) || - (_split_direction == Inkscape::SplitDirection::WEST && x > _split_position.x()) || - (_split_direction == Inkscape::SplitDirection::EAST && x < _split_position.x()) ) { - _drawing->setRenderMode(Inkscape::RenderMode::OUTLINE); - } - } - - // Convert to world coordinates. - auto p = Geom::Point(x, y) + _pos; - - _current_canvas_item_new = _canvas_item_root->pick_item(p); - // if (_current_canvas_item_new) { - // std::cout << " PICKING: FOUND ITEM: " << _current_canvas_item_new->get_name() << std::endl; - // } else { - // std::cout << " PICKING: DID NOT FIND ITEM" << std::endl; - // } - } - - if (_current_canvas_item_new == _current_canvas_item && - !_left_grabbed_item ) { - // Current item did not change! - return false; - } - - // Synthesize events for old and new current items. - bool retval = false; - if (_current_canvas_item_new != _current_canvas_item && - _current_canvas_item != nullptr && - !_left_grabbed_item ) { - - GdkEvent new_event; - new_event = _pick_event; - new_event.type = GDK_LEAVE_NOTIFY; - new_event.crossing.detail = GDK_NOTIFY_ANCESTOR; - new_event.crossing.subwindow = nullptr; - _in_repick = true; - retval = emit_event(&new_event); - _in_repick = false; - } - - if (_all_enter_events == false) { - // new_current_item may have been set to nullptr during the call to emitEvent() above. - if (_current_canvas_item_new != _current_canvas_item && - button_down ) { - _left_grabbed_item = true; - return retval; - } - } - - // Handle the rest of cases - _left_grabbed_item = false; - _current_canvas_item = _current_canvas_item_new; - - if (_current_canvas_item != nullptr ) { - GdkEvent new_event; - new_event = _pick_event; - new_event.type = GDK_ENTER_NOTIFY; - new_event.crossing.detail = GDK_NOTIFY_ANCESTOR; - new_event.crossing.subwindow = nullptr; - retval = emit_event(&new_event); - } - - return retval; -} - -bool -Canvas::emit_event(GdkEvent *event) -{ - framecheck_whole_function(d) - - if (event == d->ignore) { - return false; - } - - Gdk::EventMask mask = (Gdk::EventMask)0; - if (_grabbed_canvas_item) { - switch (event->type) { - case GDK_ENTER_NOTIFY: - mask = Gdk::ENTER_NOTIFY_MASK; - break; - case GDK_LEAVE_NOTIFY: - mask = Gdk::LEAVE_NOTIFY_MASK; - break; - case GDK_MOTION_NOTIFY: - mask = Gdk::POINTER_MOTION_MASK; - break; - case GDK_BUTTON_PRESS: - case GDK_2BUTTON_PRESS: - case GDK_3BUTTON_PRESS: - mask = Gdk::BUTTON_PRESS_MASK; - break; - case GDK_BUTTON_RELEASE: - mask = Gdk::BUTTON_RELEASE_MASK; - break; - case GDK_KEY_PRESS: - mask = Gdk::KEY_PRESS_MASK; - break; - case GDK_KEY_RELEASE: - mask = Gdk::KEY_RELEASE_MASK; - break; - case GDK_SCROLL: - mask = Gdk::SCROLL_MASK; - mask |= Gdk::SMOOTH_SCROLL_MASK; - break; - default: - break; - } - - if (!(mask & _grabbed_event_mask)) { - return false; - } - } - - // Convert to world coordinates. We have two different cases due to - // different event structures. - GdkEvent *event_copy = gdk_event_copy(event); - switch (event_copy->type) { - case GDK_ENTER_NOTIFY: - case GDK_LEAVE_NOTIFY: - event_copy->crossing.x += _pos.x(); - event_copy->crossing.y += _pos.y(); - break; - case GDK_MOTION_NOTIFY: - case GDK_BUTTON_PRESS: - case GDK_2BUTTON_PRESS: - case GDK_3BUTTON_PRESS: - case GDK_BUTTON_RELEASE: - event_copy->motion.x += _pos.x(); - event_copy->motion.y += _pos.y(); - break; - default: - break; - } - - d->bucket.emplace_back(event_copy); - if (!d->pending_bucket_emptier) { - add_tick_callback([this] (const Glib::RefPtr&) { - d->schedule_bucket_emptier(); - d->pending_bucket_emptier = true; - return false; - }); - } - - return true; -} - } // namespace Widget } // namespace UI } // namespace Inkscape diff --git a/src/ui/widget/canvas.h b/src/ui/widget/canvas.h index deea8a367b..2fb53b4394 100644 --- a/src/ui/widget/canvas.h +++ b/src/ui/widget/canvas.h @@ -145,10 +145,6 @@ private: void add_clippath(const Cairo::RefPtr& cr); void set_cursor(); - // Events - bool pick_current_item(GdkEvent *event); - bool emit_event(GdkEvent *event); - // ====== Data members ======= // Structure -- GitLab From 956789ecc832adc9cc1d914e6ea62762955e41fc Mon Sep 17 00:00:00 2001 From: PBS Date: Sat, 29 Jan 2022 01:10:28 +0900 Subject: [PATCH 34/35] Developer mode master switch You can now toggle a switch in preferences, off by default, which both hides the debug settings thefrom the UI and masks any modifications made to them until developer mode is turned back on again. --- src/ui/dialog/inkscape-preferences.cpp | 128 ++++++++---- src/ui/dialog/inkscape-preferences.h | 6 +- src/ui/widget/canvas.cpp | 267 ++++++++++++++----------- src/ui/widget/canvas.h | 16 +- 4 files changed, 245 insertions(+), 172 deletions(-) diff --git a/src/ui/dialog/inkscape-preferences.cpp b/src/ui/dialog/inkscape-preferences.cpp index 2d64bc3fee..448d00efec 100644 --- a/src/ui/dialog/inkscape-preferences.cpp +++ b/src/ui/dialog/inkscape-preferences.cpp @@ -2634,23 +2634,25 @@ void InkscapePreferences::initPageRendering() _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 size - _rendering_tile_size.init("/options/rendering/tile-size", 1.0, 10000.0, 1.0, 0.0, 16.0, true, false); - _page_rendering.add_line( false, _("Tile size:"), _rendering_tile_size, "", _("The \"tile size\" parameter previously hard-coded into Inkscape's original tile bisector."), 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 xray radius + // 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); - // rendering outline overlay opcaity + // rendering outline overlay opacity _rendering_outline_overlay_opacity.init("/options/rendering/outline-overlay-opacity", 1.0, 100.0, 1.0, 5.0, 50.0, true, false); _rendering_outline_overlay_opacity.signal_focus_out_event().connect(sigc::mem_fun(*this, &InkscapePreferences::on_outline_overlay_changed)); _page_rendering.add_line( false, _("Outline overlay opacity:"), _rendering_outline_overlay_opacity, _("%"), _("Opacity of the color in outline overlay view mode"), false); + // update strategy + int values[] = {1, 2, 3}; + Glib::ustring labels[] = {_("Responsive"), _("Full redraw"), _("Multiscale")}; + _canvas_update_strategy.init("/options/rendering/update_strategy", labels, values, 3, 3); + _page_rendering.add_line(true, _("Update strategy:"), _canvas_update_strategy, "", _("How to update continually changing content when it can't be redrawn fast enough"), false); + /* blur quality */ _blur_quality_best.init ( _("Best quality (slowest)"), "/options/blurquality/value", BLUR_QUALITY_BEST, false, nullptr); @@ -2699,49 +2701,93 @@ void InkscapePreferences::initPageRendering() _page_rendering.add_line( true, "", _filter_quality_worst, "", _("Lowest quality (considerable artifacts), but display is fastest")); - _page_rendering.add_group_header(_("Low-level tuning options")); - int values[] = {1, 2, 3}; - Glib::ustring labels[] = {_("Responsive"), _("Full redraw"), _("Multiscale")}; - _canvas_update_strategy.init("/options/rendering/update_strategy", labels, values, 3, 3); - _page_rendering.add_line(true, _("Update strategy:"), _canvas_update_strategy, "", _("How to update changing content when drawing is not fast enough."), false); + auto grid = Gtk::make_managed(); + grid->set_border_width(12); + grid->set_orientation(Gtk::ORIENTATION_VERTICAL); + grid->set_column_spacing(12); + grid->set_row_spacing(6); + auto revealer = Gtk::make_managed(); + 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());}); + _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) { + widget.set_tooltip_text(tip); + + auto hb = Gtk::manage(new Gtk::Box()); + hb->set_spacing(12); + hb->set_hexpand(true); + hb->pack_start(widget, false, false); + hb->set_valign(Gtk::ALIGN_CENTER); + + auto label_widget = Gtk::make_managed(label, Gtk::ALIGN_START, Gtk::ALIGN_CENTER, true); + label_widget->set_mnemonic_widget(widget); + label_widget->set_markup(label_widget->get_text()); + label_widget->set_margin_start(12); + + label_widget->set_valign(Gtk::ALIGN_CENTER); + grid->add(*label_widget); + grid->attach_next_to(*hb, *label_widget, Gtk::POS_RIGHT, 1, 1); + + if (suffix != "") { + 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 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); + grid->add(*label_widget); + }; + + 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); - _page_rendering.add_line(true, _("Render time limit"), _canvas_render_time_limit, C_("microsecond abbreviation", "μs"), _("The maximum time allowed for a rendering time slice"), false); - _canvas_use_new_bisector.init(_("Use new bisector"), "/options/rendering/use_new_bisector", true); - _page_rendering.add_line(true, "", _canvas_use_new_bisector, "", _("Use an alternative, more obvious bisection strategy: just chop in half along the larger dimension until small enough")); + 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"), _canvas_use_new_bisector, "", _("Use an alternative, more obvious bisection strategy: just chop 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); - _page_rendering.add_line(true, _("New bisector tile size"), _canvas_new_bisector_size, C_("pixel abbreviation", "px"), _("Chop rectangles until largest dimension is this small"), false); + add_devmode_line(_("New bisector tile size"), _canvas_new_bisector_size, C_("pixel abbreviation", "px"), _("Chop rectangles until 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_max_affine_diff.init("/options/rendering/max_affine_diff", 0.0, 100.0, 0.1, 0.0, 1.8, false, false); - _page_rendering.add_line(true, _("Max affine diff"), _canvas_max_affine_diff, "", _("How much the viewing transformation can change before throwing away the current redraw and starting again"), false); + add_devmode_line(_("Max affine diff"), _canvas_max_affine_diff, "", _("How much the viewing transformation can change before throwing away the current redraw and starting again")); _canvas_pad.init("/options/rendering/pad", 0.0, 1000.0, 1.0, 0.0, 200.0, true, false); - _page_rendering.add_line(true, _("Buffer padding"), _canvas_pad, C_("pixel abbreviation", "px"), _("Use buffers bigger than the window by this amount"), false); + add_devmode_line(_("Buffer padding"), _canvas_pad, C_("pixel abbreviation", "px"), _("Use buffers bigger than the window by this amount")); _canvas_coarsener_min_size.init("/options/rendering/coarsener_min_size", 0.0, 1000.0, 1.0, 0.0, 200.0, true, false); - _page_rendering.add_line(true, _("Coarsener min size"), _canvas_coarsener_min_size, C_("pixel abbreviation", "px"), _("Only coarsen rectangles smaller/thinner than this."), false); + add_devmode_line(_("Coarsener min size"), _canvas_coarsener_min_size, C_("pixel abbreviation", "px"), _("Only coarsen rectangles smaller/thinner than this.")); _canvas_coarsener_glue_size.init("/options/rendering/coarsener_glue_size", 0.0, 1000.0, 1.0, 0.0, 80.0, true, false); - _page_rendering.add_line(true, _("Coarsener glue size"), _canvas_coarsener_glue_size, C_("pixel abbreviation", "px"), _("Absorb nearby rectangles within this distance."), false); + add_devmode_line(_("Coarsener glue size"), _canvas_coarsener_glue_size, C_("pixel abbreviation", "px"), _("Absorb 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); - _page_rendering.add_line(true, _("Coarsener min fullness"), _canvas_coarsener_min_fullness, "", _("Refuse coarsening attempt if result would be more empty than this."), false); - - _page_rendering.add_group_header(_("Debugging, profiling, and experiments")); - _canvas_debug_framecheck.init(_("Framecheck"), "/options/rendering/debug_framecheck", false); - _page_rendering.add_line(true, "", _canvas_debug_framecheck, "", _("Print profiling data of selected operations to a file")); - _canvas_debug_logging.init(_("Logging"), "/options/rendering/debug_logging", false); - _page_rendering.add_line(true, "", _canvas_debug_logging, "", _("Log certain events to the console")); - _canvas_debug_slow_redraw.init(_("Slow redraw"), "/options/rendering/debug_slow_redraw", false); - _page_rendering.add_line(true, "", _canvas_debug_slow_redraw, "", _("Introduce a fixed delay for each tile")); + add_devmode_line(_("Coarsener min fullness"), _canvas_coarsener_min_fullness, "", _("Refuse coarsening attempt if result would be more empty than this.")); + + 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); - _page_rendering.add_line(true, _("Slow redraw time"), _canvas_debug_slow_redraw_time, C_("microsecond abbreviation", "μs"), _("The delay to introduce for each tile"), false); - _canvas_debug_show_redraw.init(_("Show redraw"), "/options/rendering/debug_show_redraw", false); - _page_rendering.add_line(true, "", _canvas_debug_show_redraw, "", _("Paint a translucent random colour over each newly drawn tile")); - _canvas_debug_show_unclean.init(_("Show unclean region"), "/options/rendering/debug_show_unclean", false); - _page_rendering.add_line(true, "", _canvas_debug_show_unclean, "", _("Show the unclean region in red")); - _canvas_debug_show_snapshot.init(_("Show snapshot"), "/options/rendering/debug_show_snapshot", false); - _page_rendering.add_line(true, "", _canvas_debug_show_snapshot, "", _("Show the snapshot region in blue")); - _canvas_debug_show_clean.init(_("Show clean fragmentation"), "/options/rendering/debug_show_clean", false); - _page_rendering.add_line(true, "", _canvas_debug_show_clean, "", _("Show the outlines of the rectangles in the clean region in green")); - _canvas_debug_disable_redraw.init(_("Disable redraw"), "/options/rendering/debug_disable_redraw", false); - _page_rendering.add_line(true, "", _canvas_debug_disable_redraw, "", _("Temporarily disable the idle redraw process completely")); - _canvas_debug_sticky_decoupled.init(_("Sticky decoupled mode"), "/options/rendering/debug_sticky_decoupled", false); - _page_rendering.add_line(true, "", _canvas_debug_sticky_decoupled, "", _("Stay in decoupled mode even after rendering is complete")); + add_devmode_line(_("Slow redraw time"), _canvas_debug_slow_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); + add_devmode_line(_("Show unclean region"), _canvas_debug_show_unclean, "", _("Show the unclean region in red")); + _canvas_debug_show_snapshot.init("", "/options/rendering/debug_show_snapshot", false); + add_devmode_line(_("Show snapshot"), _canvas_debug_show_snapshot, "", _("Show the snapshot region in blue")); + _canvas_debug_show_clean.init("", "/options/rendering/debug_show_clean", false); + add_devmode_line(_("Show clean fragmentation"), _canvas_debug_show_clean, "", _("Show the outlines of the rectangles in the clean region in green")); + _canvas_debug_disable_redraw.init("", "/options/rendering/debug_disable_redraw", false); + add_devmode_line(_("Disable redraw"), _canvas_debug_disable_redraw, "", _("Temporarily disable the idle redraw process completely")); + _canvas_debug_sticky_decoupled.init("", "/options/rendering/debug_sticky_decoupled", false); + add_devmode_line(_("Sticky decoupled mode"), _canvas_debug_sticky_decoupled, "", _("Stay in decoupled mode even after rendering is complete")); this->AddPage(_page_rendering, _("Rendering"), PREFS_PAGE_RENDERING); } diff --git a/src/ui/dialog/inkscape-preferences.h b/src/ui/dialog/inkscape-preferences.h index 919bc47cde..8978e9962e 100644 --- a/src/ui/dialog/inkscape-preferences.h +++ b/src/ui/dialog/inkscape-preferences.h @@ -323,9 +323,9 @@ protected: UI::Widget::PrefSpinButton _filter_multi_threaded; UI::Widget::PrefSpinButton _rendering_cache_size; UI::Widget::PrefSpinButton _rendering_tile_multiplier; - UI::Widget::PrefSpinButton _rendering_tile_size; UI::Widget::PrefSpinButton _rendering_xray_radius; UI::Widget::PrefSpinButton _rendering_outline_overlay_opacity; + UI::Widget::PrefCombo _canvas_update_strategy; UI::Widget::PrefRadioButton _blur_quality_best; UI::Widget::PrefRadioButton _blur_quality_better; UI::Widget::PrefRadioButton _blur_quality_normal; @@ -337,16 +337,16 @@ protected: UI::Widget::PrefRadioButton _filter_quality_worse; UI::Widget::PrefRadioButton _filter_quality_worst; - UI::Widget::PrefCombo _canvas_update_strategy; + UI::Widget::PrefCheckButton _canvas_developer_mode_enabled; 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_max_affine_diff; UI::Widget::PrefSpinButton _canvas_pad; UI::Widget::PrefSpinButton _canvas_coarsener_min_size; UI::Widget::PrefSpinButton _canvas_coarsener_glue_size; UI::Widget::PrefSpinButton _canvas_coarsener_min_fullness; - UI::Widget::PrefCheckButton _canvas_debug_framecheck; UI::Widget::PrefCheckButton _canvas_debug_logging; UI::Widget::PrefCheckButton _canvas_debug_slow_redraw; diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index a8f66bf055..0895ed7217 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -83,49 +83,48 @@ namespace Widget { * Preferences system */ +template +struct Pref {}; + template struct PrefBase { - T t; + const char *path; + T t, def; std::unique_ptr obs; std::function action; operator T() const {return t;} + PrefBase(const char *path, T def) : path(path), def(def) {enable();} + void act() {if (action) action();} + void enable() {t = static_cast*>(this)->read(); act(); obs = Inkscape::Preferences::get()->createObserver(path, [this] (const Preferences::Entry &e) {t = static_cast*>(this)->changed(e); act();});} + void disable() {t = def; act(); obs.reset();} + void set_enabled(bool enabled) {enabled ? enable() : disable();} }; -template -struct Pref {}; - template<> struct Pref : PrefBase { - Pref(const char *path, bool def = false) - { - auto prefs = Inkscape::Preferences::get(); - t = prefs->getBool(path, def); - obs = prefs->createObserver(path, [=] (const Preferences::Entry &e) {t = e.getBool(def); if (action) action();}); - } + Pref(const char *path, bool def = false) : PrefBase(path, def) {} + bool read() {return Inkscape::Preferences::get()->getBool(path, def);} + bool changed(const Preferences::Entry &e) {return e.getBool(def);} }; template<> struct Pref : PrefBase { - Pref(const char *path, int def, int min, int max) - { - auto prefs = Inkscape::Preferences::get(); - t = prefs->getIntLimited(path, def, min, max); - obs = prefs->createObserver(path, [=] (const Preferences::Entry &e) {t = e.getIntLimited(def, min, max); if (action) action();}); - } + int min, max; + Pref(const char *path, int def, int min, int max) : min(min), max(max), PrefBase(path, def) {} + int read() {return Inkscape::Preferences::get()->getIntLimited(path, def, min, max);} + int changed(const Preferences::Entry &e) {return e.getIntLimited(def, min, max);} }; template<> struct Pref : PrefBase { - Pref(const char *path, double def, double min, double max) - { - auto prefs = Inkscape::Preferences::get(); - t = prefs->getDoubleLimited(path, def, min, max); - obs = prefs->createObserver(path, [=] (const Preferences::Entry &e) {t = e.getDoubleLimited(def, min, max); if (action) action();}); - } + double min, max; + Pref(const char *path, double def, double min, double max) : min(min), max(max), PrefBase(path, def) {} + double read() {return Inkscape::Preferences::get()->getDoubleLimited(path, def, min, max);} + double changed(const Preferences::Entry &e) {return e.getDoubleLimited(def, min, max);} }; struct Prefs @@ -160,8 +159,35 @@ struct Prefs Pref debug_show_clean = Pref ("/options/rendering/debug_show_clean"); Pref debug_disable_redraw = Pref ("/options/rendering/debug_disable_redraw"); Pref debug_sticky_decoupled = Pref ("/options/rendering/debug_sticky_decoupled"); + + // Developer mode + Pref devmode = Pref("/options/rendering/devmode"); + void set_devmode(bool on); }; +void Prefs::set_devmode(bool on) +{ + tile_size.set_enabled(on); + render_time_limit.set_enabled(on); + use_new_bisector.set_enabled(on); + new_bisector_size.set_enabled(on); + max_affine_diff.set_enabled(on); + pad.set_enabled(on); + coarsener_min_size.set_enabled(on); + coarsener_glue_size.set_enabled(on); + coarsener_min_fullness.set_enabled(on); + debug_framecheck.set_enabled(on); + debug_logging.set_enabled(on); + debug_slow_redraw.set_enabled(on); + debug_slow_redraw_time.set_enabled(on); + debug_show_redraw.set_enabled(on); + debug_show_unclean.set_enabled(on); + debug_show_snapshot.set_enabled(on); + debug_show_clean.set_enabled(on); + debug_disable_redraw.set_enabled(on); + debug_sticky_decoupled.set_enabled(on); +} + /* * Conversion functions */ @@ -1143,14 +1169,9 @@ CanvasPrivate::emit_event(const GdkEvent &event) } /* - * The Rest + * Canvas */ -void CanvasPrivate::queue_draw_area(Geom::IntRect &rect) -{ - q->queue_draw_area(rect.left(), rect.top(), rect.width(), rect.height()); -} - Canvas::Canvas() : d(std::make_unique(this)) { @@ -1177,6 +1198,10 @@ Canvas::Canvas() d->prefs.update_strategy.action = [=] {d->updater = make_updater(d->prefs.update_strategy, std::move(d->updater->clean_region));}; d->prefs.outline_overlay_opacity.action = [=] {queue_draw();}; + // Developer mode master switch + d->prefs.devmode.action = [=] {d->prefs.set_devmode(d->prefs.devmode);}; + d->prefs.devmode.action(); + // Give _pick_event an initial definition. _pick_event.type = GDK_LEAVE_NOTIFY; _pick_event.crossing.x = 0; @@ -1208,7 +1233,8 @@ Canvas::~Canvas() delete _canvas_item_root; } -Geom::IntPoint Canvas::get_dimensions() const +Geom::IntPoint +Canvas::get_dimensions() const { Gtk::Allocation allocation = get_allocation(); return {allocation.get_width(), allocation.get_height()}; @@ -1257,6 +1283,11 @@ Canvas::set_affine(Geom::Affine const &affine) queue_draw(); } +void CanvasPrivate::queue_draw_area(Geom::IntRect &rect) +{ + q->queue_draw_area(rect.left(), rect.top(), rect.width(), rect.height()); +} + /** * Invalidate drawing and redraw during idle. */ @@ -1441,7 +1472,8 @@ Canvas::set_split_direction(Inkscape::SplitDirection dir) } } -Cairo::RefPtr Canvas::get_backing_store() const +Cairo::RefPtr +Canvas::get_backing_store() const { return d->_backing_store; } @@ -1470,7 +1502,51 @@ Canvas::canvas_item_clear(Inkscape::CanvasItem* item) } } -// ============== Protected Functions ============== +// Change cursor +void +Canvas::set_cursor() { + + if (!_desktop) { + return; + } + + auto display = Gdk::Display::get_default(); + + switch (_hover_direction) { + + case Inkscape::SplitDirection::NONE: + _desktop->event_context->use_tool_cursor(); + break; + + case Inkscape::SplitDirection::NORTH: + case Inkscape::SplitDirection::EAST: + case Inkscape::SplitDirection::SOUTH: + case Inkscape::SplitDirection::WEST: + { + auto cursor = Gdk::Cursor::create(display, "pointer"); + get_window()->set_cursor(cursor); + break; + } + + case Inkscape::SplitDirection::HORIZONTAL: + { + auto cursor = Gdk::Cursor::create(display, "ns-resize"); + get_window()->set_cursor(cursor); + break; + } + + case Inkscape::SplitDirection::VERTICAL: + { + auto cursor = Gdk::Cursor::create(display, "ew-resize"); + get_window()->set_cursor(cursor); + break; + } + + default: + // Shouldn't reach. + std::cerr << "Canvas::set_cursor: Unknown hover direction!" << std::endl; + } +} void Canvas::get_preferred_width_vfunc(int &minimum_width, int &natural_width) const @@ -1484,22 +1560,25 @@ Canvas::get_preferred_height_vfunc(int &minimum_height, int &natural_height) con minimum_height = natural_height = 256; } -/** - * Resize handler - */ -void Canvas::on_size_allocate(Gtk::Allocation &allocation) +void +Canvas::on_size_allocate(Gtk::Allocation &allocation) { parent_type::on_size_allocate(allocation); assert(allocation == get_allocation()); d->add_idle(); // Trigger the size update to be applied to the stores before the next call to on_draw. } -void Canvas::on_realize() +void +Canvas::on_realize() { parent_type::on_realize(); d->add_idle(); } +/* + * Drawing + */ + /* * The on_draw() function is called whenever Gtk wants to update the window. This function: * @@ -1749,6 +1828,41 @@ Canvas::on_draw(const Cairo::RefPtr<::Cairo::Context> &cr) return true; } +// Sets clip path for Split and X-Ray modes. +void +Canvas::add_clippath(const Cairo::RefPtr& cr) +{ + double width = get_allocation().get_width(); + double height = get_allocation().get_height(); + double sx = _split_position.x(); + double sy = _split_position.y(); + + if (_split_mode == Inkscape::SplitMode::SPLIT) { + // We're clipping the outline region... so it's backwards. + switch (_split_direction) { + case Inkscape::SplitDirection::SOUTH: + cr->rectangle(0, 0, width, sy); + break; + case Inkscape::SplitDirection::NORTH: + cr->rectangle(0, sy, width, height - sy); + break; + case Inkscape::SplitDirection::EAST: + cr->rectangle(0, 0, sx, height ); + break; + case Inkscape::SplitDirection::WEST: + cr->rectangle(sx, 0, width - sx, height ); + break; + default: + // no clipping (for NONE, HORIZONTAL, VERTICAL) + break; + } + } else { + cr->arc(sx, sy, d->prefs.x_ray_radius, 0, 2 * M_PI); + } + + cr->clip(); +} + void CanvasPrivate::add_idle() { @@ -2452,87 +2566,6 @@ CanvasPrivate::paint_single_buffer(Geom::IntRect const &paint_rect, const Cairo: store->mark_dirty(); } -// Sets clip path for Split and X-Ray modes. -void -Canvas::add_clippath(const Cairo::RefPtr& cr) -{ - double width = get_allocation().get_width(); - double height = get_allocation().get_height(); - double sx = _split_position.x(); - double sy = _split_position.y(); - - if (_split_mode == Inkscape::SplitMode::SPLIT) { - // We're clipping the outline region... so it's backwards. - switch (_split_direction) { - case Inkscape::SplitDirection::SOUTH: - cr->rectangle(0, 0, width, sy); - break; - case Inkscape::SplitDirection::NORTH: - cr->rectangle(0, sy, width, height - sy); - break; - case Inkscape::SplitDirection::EAST: - cr->rectangle(0, 0, sx, height ); - break; - case Inkscape::SplitDirection::WEST: - cr->rectangle(sx, 0, width - sx, height ); - break; - default: - // no clipping (for NONE, HORIZONTAL, VERTICAL) - break; - } - } else { - cr->arc(sx, sy, d->prefs.x_ray_radius, 0, 2 * M_PI); - } - - cr->clip(); -} - -// Change cursor -void -Canvas::set_cursor() { - - if (!_desktop) { - return; - } - - auto display = Gdk::Display::get_default(); - - switch (_hover_direction) { - - case Inkscape::SplitDirection::NONE: - _desktop->event_context->use_tool_cursor(); - break; - - case Inkscape::SplitDirection::NORTH: - case Inkscape::SplitDirection::EAST: - case Inkscape::SplitDirection::SOUTH: - case Inkscape::SplitDirection::WEST: - { - auto cursor = Gdk::Cursor::create(display, "pointer"); - get_window()->set_cursor(cursor); - break; - } - - case Inkscape::SplitDirection::HORIZONTAL: - { - auto cursor = Gdk::Cursor::create(display, "ns-resize"); - get_window()->set_cursor(cursor); - break; - } - - case Inkscape::SplitDirection::VERTICAL: - { - auto cursor = Gdk::Cursor::create(display, "ew-resize"); - get_window()->set_cursor(cursor); - break; - } - - default: - // Shouldn't reach. - std::cerr << "Canvas::set_cursor: Unknown hover direction!" << std::endl; - } -} - } // namespace Widget } // namespace UI } // namespace Inkscape diff --git a/src/ui/widget/canvas.h b/src/ui/widget/canvas.h index 2fb53b4394..412099e5c2 100644 --- a/src/ui/widget/canvas.h +++ b/src/ui/widget/canvas.h @@ -64,6 +64,8 @@ public: void set_drawing(Inkscape::Drawing *drawing) { _drawing = drawing; } 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(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. void scroll_to(Geom::Point const &c); @@ -116,7 +118,6 @@ public: void set_all_enter_events(bool on) { _all_enter_events = on; } protected: - void get_preferred_width_vfunc (int &minimum_width, int &natural_width ) const override; void get_preferred_height_vfunc(int &minimum_height, int &natural_height) const override; @@ -137,16 +138,9 @@ protected: bool on_draw(const Cairo::RefPtr&) override; private: - - // Drawing (internal overloads) - 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 add_clippath(const Cairo::RefPtr& cr); void set_cursor(); - // ====== Data members ======= - // Structure SPDesktop *_desktop = nullptr; @@ -177,12 +171,12 @@ private: Inkscape::RenderMode _render_mode = Inkscape::RenderMode::NORMAL; Inkscape::SplitMode _split_mode = Inkscape::SplitMode::NORMAL; + Inkscape::ColorMode _color_mode = Inkscape::ColorMode::NORMAL; Geom::Point _split_position{-1, -1}; // initialize with off-canvas coordinates - Inkscape::SplitDirection _split_direction = Inkscape::SplitDirection::EAST; - Inkscape::SplitDirection _hover_direction = Inkscape::SplitDirection::NONE; + Inkscape::SplitDirection _split_direction = Inkscape::SplitDirection::EAST; + Inkscape::SplitDirection _hover_direction = Inkscape::SplitDirection::NONE; bool _split_dragging = false; Geom::Point _split_drag_start; - Inkscape::ColorMode _color_mode = Inkscape::ColorMode::NORMAL; std::string _cms_key; bool _cms_active = false; -- GitLab From 2673bcb253dab97823582651bfd2cebdd6052e00 Mon Sep 17 00:00:00 2001 From: PBS Date: Sun, 30 Jan 2022 11:40:38 +0900 Subject: [PATCH 35/35] Fix #3172, clean up a header --- src/ui/tool/control-point-selection.cpp | 1 - src/ui/tools/select-tool.h | 46 ++++++++++++------------- src/ui/widget/canvas.cpp | 16 ++++----- 3 files changed, 31 insertions(+), 32 deletions(-) diff --git a/src/ui/tool/control-point-selection.cpp b/src/ui/tool/control-point-selection.cpp index e968e1c1af..3073b6bec8 100644 --- a/src/ui/tool/control-point-selection.cpp +++ b/src/ui/tool/control-point-selection.cpp @@ -19,7 +19,6 @@ #include "ui/tool/transform-handle-set.h" #include "ui/tool/node.h" #include "display/control/snap-indicator.h" -#include "ui/tools/tool-base.h" #include "ui/widget/canvas.h" diff --git a/src/ui/tools/select-tool.h b/src/ui/tools/select-tool.h index e73a082fc8..76355be6b3 100644 --- a/src/ui/tools/select-tool.h +++ b/src/ui/tools/select-tool.h @@ -29,35 +29,35 @@ namespace Tools { class SelectTool : public ToolBase { public: - SelectTool(); - ~SelectTool() override; + SelectTool(); + ~SelectTool() override; - bool dragging; - bool moved; - guint button_press_state; + bool dragging; + bool moved; + guint button_press_state; - std::vector cycling_items; - std::vector cycling_items_cmp; - SPItem *cycling_cur_item; - bool cycling_wrap; + std::vector cycling_items; + std::vector cycling_items_cmp; + SPItem *cycling_cur_item; + bool cycling_wrap; - SPItem *item; + SPItem *item; Inkscape::CanvasItem *grabbed = nullptr; - Inkscape::SelTrans *_seltrans; - Inkscape::SelectionDescriber *_describer; + Inkscape::SelTrans *_seltrans; + Inkscape::SelectionDescriber *_describer; gchar *no_selection_msg = nullptr; - void setup() override; - void set(const Inkscape::Preferences::Entry& val) override; - bool root_handler(GdkEvent* event) override; - bool item_handler(SPItem* item, GdkEvent* event) override; + void setup() override; + void set(const Inkscape::Preferences::Entry &val) override; + bool root_handler(GdkEvent* event) override; + bool item_handler(SPItem* item, GdkEvent *event) override; - const std::string& getPrefsPath() override; + const std::string& getPrefsPath() override; private: - bool sp_select_context_abort(); - void sp_select_context_cycle_through_items(Inkscape::Selection *selection, GdkEventScroll *scroll_event); - void sp_select_context_reset_opacities(); + bool sp_select_context_abort(); + void sp_select_context_cycle_through_items(Inkscape::Selection *selection, GdkEventScroll *scroll_event); + void sp_select_context_reset_opacities(); bool _alt_on; bool _force_dragging; @@ -65,9 +65,9 @@ private: std::string _default_cursor; }; -} -} -} +} // namespace Tools +} // namespace UI +} // namespace Inkscape #endif diff --git a/src/ui/widget/canvas.cpp b/src/ui/widget/canvas.cpp index 0895ed7217..4d3ee1a0da 100644 --- a/src/ui/widget/canvas.cpp +++ b/src/ui/widget/canvas.cpp @@ -497,6 +497,9 @@ public: // Trivial overload of GtkWidget function. void queue_draw_area(Geom::IntRect &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; }; /* @@ -584,6 +587,7 @@ Canvas::on_leave_notify_event(GdkEventCrossing *crossing_event) std::cout << " WHOOPS... this does really happen" << std::endl; return false; } + d->last_mouse = {}; return d->add_to_bucket(reinterpret_cast(crossing_event)); } @@ -609,6 +613,9 @@ Canvas::on_key_release_event(GdkEventKey *key_event) bool Canvas::on_motion_notify_event(GdkEventMotion *motion_event) { + // Record the last mouse position. + d->last_mouse = Geom::Point(motion_event->x, motion_event->y); + // Handle interactions with the split view controller. if (_desktop) { Geom::IntPoint cursor_position = Geom::IntPoint(motion_event->x, motion_event->y); @@ -2309,14 +2316,7 @@ CanvasPrivate::on_idle() assert(_store_rect.contains(visible_rect)); // Get the mouse position in screen space. - Geom::IntPoint mouse_loc; - if (auto window = q->get_window()) { - int x; - int y; - Gdk::ModifierType mask; - window->get_device_position(Gdk::Display::get_default()->get_default_seat()->get_pointer(), x, y, mask); - mouse_loc = Geom::IntPoint(x, y); - } + Geom::IntPoint mouse_loc = (last_mouse ? *last_mouse : Geom::Point(q->get_dimensions()) / 2).round(); // Map the mouse to canvas space. mouse_loc += q->_pos; -- GitLab