From 0aa400b1d8d8bf933396e74af9a4248b6c92287b Mon Sep 17 00:00:00 2001 From: Behdad Esfahbod Date: Sat, 15 Feb 2025 23:19:44 -0700 Subject: [PATCH] [decycler] Implement an efficient graph cycle detector This is an algorithm I came up with, based on the Floyd's Tortoise-Hare constant-memory linear-time linked-list cycle-detection algorithm. https://en.wikipedia.org/wiki/Cycle_detection#Floyd's_tortoise_and_hare It is linear-time and malloc-free. It *eventually* detects cycles, not immediately. The main different with Floyd's algorithm is that this algorithm detects cycles when one is traversing down a graph, not just a linked list. Our existing cycle-detection algorithms use a set-of-integers, either hb_set_t, or more efficient in this case, hb_map_t. Those include at least one malloc, and as such show up on profiles. Port hb-ot-font COLRv1 to use the decycler instead of previous hb_map_t usage for cycle detection. benchmark-font paint_glyph on NotoColorEmoji-Regular.ttf: Before: 8ms; After: 5.5ms. No cycle detection: 5.5ms. FT COLRv1 API is so slow (174ms) it's not worth porting to this. Other graphs (VARC, etc) to be ported. Test and documentation to be added. --- src/OT/Color/COLR/COLR.hh | 26 +++++----- src/hb-decycler.hh | 100 ++++++++++++++++++++++++++++++++++++++ src/meson.build | 1 + 3 files changed, 112 insertions(+), 15 deletions(-) create mode 100644 src/hb-decycler.hh diff --git a/src/OT/Color/COLR/COLR.hh b/src/OT/Color/COLR/COLR.hh index 6af7be990..bc0d6756a 100644 --- a/src/OT/Color/COLR/COLR.hh +++ b/src/OT/Color/COLR/COLR.hh @@ -29,6 +29,7 @@ #define OT_COLOR_COLR_COLR_HH #include "../../../hb.hh" +#include "../../../hb-decycler.hh" #include "../../../hb-open-type.hh" #include "../../../hb-ot-var-common.hh" #include "../../../hb-paint.hh" @@ -71,8 +72,8 @@ public: hb_array_t palette; hb_color_t foreground; ItemVarStoreInstancer &instancer; - hb_map_t current_glyphs; - hb_map_t current_layers; + hb_decycler_t glyphs_decycler; + hb_decycler_t layers_decycler; int depth_left = HB_MAX_NESTING_LEVEL; int edge_count = HB_MAX_GRAPH_EDGE_COUNT; @@ -2576,7 +2577,9 @@ struct COLR &(get_delta_set_index_map ()), hb_array (font->coords, font->num_coords)); hb_paint_context_t c (this, funcs, data, font, palette_index, foreground, instancer); - c.current_glyphs.add (glyph); + + hb_decycler_node_t node (c.glyphs_decycler); + node.visit (glyph); if (version >= 1) { @@ -2692,19 +2695,16 @@ void PaintColrLayers::paint_glyph (hb_paint_context_t *c) const { TRACE_PAINT (this); const LayerList &paint_offset_lists = c->get_colr_table ()->get_layerList (); + hb_decycler_node_t node (c->layers_decycler); for (unsigned i = firstLayerIndex; i < firstLayerIndex + numLayers; i++) { - if (unlikely (c->current_layers.has (i))) - continue; - - c->current_layers.add (i); + if (unlikely (!node.visit (i))) + return; const Paint &paint = paint_offset_lists.get_paint (i); c->funcs->push_group (c->data); c->recurse (paint); c->funcs->pop_group (c->data, HB_PAINT_COMPOSITE_MODE_SRC_OVER); - - c->current_layers.del (i); } } @@ -2712,16 +2712,14 @@ void PaintColrGlyph::paint_glyph (hb_paint_context_t *c) const { TRACE_PAINT (this); - if (unlikely (c->current_glyphs.has (gid))) + hb_decycler_node_t node (c->glyphs_decycler); + if (unlikely (!node.visit (gid))) return; - c->current_glyphs.add (gid); - c->funcs->push_inverse_root_transform (c->data, c->font); if (c->funcs->color_glyph (c->data, gid, c->font)) { c->funcs->pop_transform (c->data); - c->current_glyphs.del (gid); return; } c->funcs->pop_transform (c->data); @@ -2744,8 +2742,6 @@ void PaintColrGlyph::paint_glyph (hb_paint_context_t *c) const if (has_clip_box) c->funcs->pop_clip (c->data); - - c->current_glyphs.del (gid); } } /* namespace OT */ diff --git a/src/hb-decycler.hh b/src/hb-decycler.hh new file mode 100644 index 000000000..faee8ad08 --- /dev/null +++ b/src/hb-decycler.hh @@ -0,0 +1,100 @@ +/* + * Copyright © 2025 Behdad Esfahbod + * + * This is part of HarfBuzz, a text shaping library. + * + * Permission is hereby granted, without written agreement and without + * license or royalty fees, to use, copy, modify, and distribute this + * software and its documentation for any purpose, provided that the + * above copyright notice and the following two paragraphs appear in + * all copies of this software. + * + * IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR + * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES + * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN + * IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH + * DAMAGE. + * + * THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, + * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS + * ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO + * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + * + * Author(s): Behdad Esfahbod + */ + +#ifndef HB_DECYCLER_HH +#define HB_DECYCLER_HH + +#include "hb.hh" + +struct hb_decycler_node_t; + +struct hb_decycler_t +{ + friend struct hb_decycler_node_t; + + private: + hb_decycler_node_t *tortoise = nullptr; + hb_decycler_node_t *hare = nullptr; + bool tortoise_asleep = false; +}; + +struct hb_decycler_node_t +{ + hb_decycler_node_t (hb_decycler_t &decycler) + : decycler (decycler) + { + snapshot = decycler; + + if (!decycler.tortoise) + { + // First node. + decycler.tortoise = decycler.hare = this; + return; + } + + decycler.hare->next = this; + decycler.hare = this; + + if (decycler.tortoise_asleep) + { + // Wake up toirtoise. + decycler.tortoise_asleep = false; + // Time to move. + decycler.tortoise = decycler.tortoise->next; + } + else + { + // Put toirtoise to sleep. + decycler.tortoise_asleep = true; + } + } + + ~hb_decycler_node_t () + { + decycler = snapshot; + } + + bool visit (unsigned value_) + { + value = value_; + + if (decycler.tortoise == this) + return true; // First node; not a cycle. + + if (decycler.tortoise->value == value) + return false; // Cycle detected. + + return true; + } + + private: + hb_decycler_t &decycler; + hb_decycler_t snapshot; + hb_decycler_node_t *next = nullptr; + unsigned value = (unsigned) -1; +}; + +#endif /* HB_DECYCLER_HH */ diff --git a/src/meson.build b/src/meson.build index b9daabf01..00132b011 100644 --- a/src/meson.build +++ b/src/meson.build @@ -43,6 +43,7 @@ hb_base_sources = files( 'hb-common.cc', 'hb-config.hh', 'hb-debug.hh', + 'hb-decycler.hh', 'hb-dispatch.hh', 'hb-draw.cc', 'hb-draw.hh',