Merge pull request #5074 from harfbuzz/tortoise-hare

[decycler] Implement an efficient graph cycle detector
This commit is contained in:
Behdad Esfahbod 2025-02-18 20:08:19 -07:00 committed by GitHub
commit 4c263ecd00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 328 additions and 56 deletions

View file

@ -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<const BGRAColor> 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;
@ -2580,7 +2581,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)
{
@ -2696,19 +2699,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);
}
}
@ -2716,16 +2716,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);
@ -2748,8 +2746,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 */

View file

@ -134,7 +134,7 @@ VarComponent::get_path_at (hb_font_t *font,
hb_array_t<const int> coords,
hb_transform_t total_transform,
hb_ubytes_t total_record,
hb_set_t *visited,
hb_decycler_t *decycler,
signed *edges_left,
signed depth_left,
VarRegionList::cache_t *cache) const
@ -319,7 +319,7 @@ VarComponent::get_path_at (hb_font_t *font,
VARC.get_path_at (font, gid,
draw_session, component_coords, total_transform,
parent_gid,
visited, edges_left, depth_left - 1);
decycler, edges_left, depth_left - 1);
}
#undef PROCESS_TRANSFORM_COMPONENTS
@ -335,17 +335,10 @@ VARC::get_path_at (hb_font_t *font,
hb_array_t<const int> coords,
hb_transform_t transform,
hb_codepoint_t parent_glyph,
hb_set_t *visited,
hb_decycler_t *decycler,
signed *edges_left,
signed depth_left) const
{
hb_set_t stack_set;
if (visited == nullptr)
visited = &stack_set;
signed stack_edges = HB_MAX_GRAPH_EDGE_COUNT;
if (edges_left == nullptr)
edges_left = &stack_edges;
// Don't recurse on the same glyph.
unsigned idx = glyph == parent_glyph ?
NOT_COVERED :
@ -377,9 +370,9 @@ VARC::get_path_at (hb_font_t *font,
return true;
(*edges_left)--;
if (visited->has (glyph) || visited->in_error ())
hb_decycler_node_t node (*decycler);
if (unlikely (!node.visit (glyph)))
return true;
visited->add (glyph);
hb_ubytes_t record = (this+glyphRecords)[idx];
@ -392,13 +385,11 @@ VARC::get_path_at (hb_font_t *font,
VarCompositeGlyph::get_path_at (font, glyph,
draw_session, coords, transform,
record,
visited, edges_left, depth_left,
decycler, edges_left, depth_left,
cache);
(this+varStore).destroy_cache (cache);
visited->del (glyph);
return true;
}

View file

@ -1,6 +1,7 @@
#ifndef OT_VAR_VARC_VARC_HH
#define OT_VAR_VARC_VARC_HH
#include "../../../hb-decycler.hh"
#include "../../../hb-geometry.hh"
#include "../../../hb-ot-layout-common.hh"
#include "../../../hb-ot-glyf-table.hh"
@ -49,7 +50,7 @@ struct VarComponent
hb_array_t<const int> coords,
hb_transform_t transform,
hb_ubytes_t record,
hb_set_t *visited,
hb_decycler_t *decycler,
signed *edges_left,
signed depth_left,
VarRegionList::cache_t *cache = nullptr) const;
@ -64,7 +65,7 @@ struct VarCompositeGlyph
hb_array_t<const int> coords,
hb_transform_t transform,
hb_ubytes_t record,
hb_set_t *visited,
hb_decycler_t *decycler,
signed *edges_left,
signed depth_left,
VarRegionList::cache_t *cache = nullptr)
@ -75,7 +76,7 @@ struct VarCompositeGlyph
record = comp.get_path_at (font, glyph,
draw_session, coords, transform,
record,
visited, edges_left, depth_left, cache);
decycler, edges_left, depth_left, cache);
}
}
};
@ -93,15 +94,27 @@ struct VARC
hb_codepoint_t glyph,
hb_draw_session_t &draw_session,
hb_array_t<const int> coords,
hb_transform_t transform = HB_TRANSFORM_IDENTITY,
hb_codepoint_t parent_glyph = HB_CODEPOINT_INVALID,
hb_set_t *visited = nullptr,
signed *edges_left = nullptr,
signed depth_left = HB_MAX_NESTING_LEVEL) const;
hb_transform_t transform,
hb_codepoint_t parent_glyph,
hb_decycler_t *decycler,
signed *edges_left,
signed depth_left) const;
bool
get_path (hb_font_t *font, hb_codepoint_t gid, hb_draw_session_t &draw_session) const
{ return get_path_at (font, gid, draw_session, hb_array (font->coords, font->num_coords)); }
{
hb_decycler_t decycler;
signed edges = HB_MAX_GRAPH_EDGE_COUNT;
return get_path_at (font,
gid,
draw_session,
hb_array (font->coords, font->num_coords),
HB_TRANSFORM_IDENTITY,
HB_CODEPOINT_INVALID,
&decycler,
&edges,
HB_MAX_NESTING_LEVEL); }
bool paint_glyph (hb_font_t *font, hb_codepoint_t gid, hb_paint_funcs_t *funcs, void *data, hb_color_t foreground) const
{

161
src/hb-decycler.hh Normal file
View file

@ -0,0 +1,161 @@
/*
* 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"
/*
* hb_decycler_t is an efficient cycle detector for graph traversal.
* It's a simple tortoise-and-hare algorithm with a twist: it's
* designed to detect cycles while traversing a graph in a DFS manner,
* instead of just a linked list.
*
* For Floyd's tortoise and hare algorithm, see:
* https://en.wikipedia.org/wiki/Cycle_detection#Floyd's_tortoise_and_hare
*
* Like Floyd's algorithm, hb_decycler_t is O(n) in the number of nodes
* in the graph. Unlike Floyd's algorithm, hb_decycler_t is designed
* to be used in a DFS traversal, where the graph is not a simple
* linked list, but a tree with cycles. Like Floyd's algorithm, it is
* constant-memory (just two pointers).
*
* The decycler works by creating an implicit linked-list on the stack,
* of the path from the root to the current node, and apply Floyd's
* algorithm on that list as it goes.
*
* The decycler is malloc-free, and as such, much faster to use than a
* hb_set_t or hb_map_t equivalent.
*
* The decycler detects cycles in the graph *eventually*, not *immediately*.
* That is, it may not detect a cycle until the cycle is fully traversed,
* even multiple times. See Floyd's algorithm analysis for details.
*
* The implementation saves a pointer storage on the stack by combining
* this->u.decycler and this->u.next into a union. This is possible because
* at any point we only need one of those values. The invariant is that
* after construction, and before destruction, of a node, the u.decycler
* field is always valid. The u.next field is only valid when the node is
* in the traversal path, parent to another node.
*
* There are three method's:
*
* - hb_decycler_node_t() constructor: Creates a new node in the traversal.
* The constructor takes a reference to the decycler object and inserts
* itself as the latest node in the traversal path, by advancing the hare
* pointer, and for every other descent, advancing the tortoise pointer.
*
* - ~hb_decycler_node_t() destructor: Restores the decycler object to its
* previous state by removing the node from the traversal path.
*
* - bool visit(uintptr_t value): Called on every node in the graph. Returns
* true if the node is not part of a cycle, and false if it is. The value
* parameter is used to detect cycles. It's the caller's responsibility
* to ensure that the value is unique for each node in the graph.
* The cycle detection is as simple as comparing the value to the value
* held by the tortoise pointer, which is the Floyd's algorithm.
*
* For usage examples see test-decycler.cc.
*/
struct hb_decycler_node_t;
struct hb_decycler_t
{
friend struct hb_decycler_node_t;
private:
bool tortoise_asleep = true;
hb_decycler_node_t *tortoise = nullptr;
hb_decycler_node_t *hare = nullptr;
};
struct hb_decycler_node_t
{
hb_decycler_node_t (hb_decycler_t &decycler)
{
u.decycler = &decycler;
decycler.tortoise_asleep = !decycler.tortoise_asleep;
if (!decycler.tortoise)
{
// First node.
decycler.tortoise = decycler.hare = this;
return;
}
if (!decycler.tortoise_asleep)
decycler.tortoise = decycler.tortoise->u.next; // Time to move.
this->prev = decycler.hare;
decycler.hare->u.next = this;
decycler.hare = this;
}
~hb_decycler_node_t ()
{
hb_decycler_t &decycler = *u.decycler;
// Inverse of the constructor.
assert (decycler.hare == this);
decycler.hare = prev;
if (prev)
prev->u.decycler = &decycler;
assert (decycler.tortoise);
if (!decycler.tortoise_asleep)
decycler.tortoise = decycler.tortoise->prev;
decycler.tortoise_asleep = !decycler.tortoise_asleep;
}
bool visit (uintptr_t value_)
{
value = value_;
hb_decycler_t &decycler = *u.decycler;
if (decycler.tortoise == this)
return true; // First node; not a cycle.
if (decycler.tortoise->value == value)
return false; // Cycle detected.
return true;
}
private:
union {
hb_decycler_t *decycler;
hb_decycler_node_t *next;
} u = {nullptr};
hb_decycler_node_t *prev = nullptr;
uintptr_t value = 0;
};
#endif /* HB_DECYCLER_HH */

View file

@ -27,6 +27,7 @@
#include "hb.hh"
#include "hb-decycler.hh"
#include "hb-paint-extents.hh"
#include FT_COLOR_H
@ -105,8 +106,8 @@ struct hb_ft_paint_context_t
FT_Color *palette;
unsigned palette_index;
hb_color_t foreground;
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;
};
@ -218,24 +219,19 @@ _hb_ft_paint (hb_ft_paint_context_t *c,
case FT_COLR_PAINTFORMAT_COLR_LAYERS:
{
FT_OpaquePaint other_paint = {0};
hb_decycler_node_t node (c->layers_decycler);
while (FT_Get_Paint_Layers (ft_face,
&paint.u.colr_layers.layer_iterator,
&other_paint))
{
// FreeType doesn't provide a way to get the layer index, so we use the pointer
// for cycle detection.
unsigned i = (unsigned) (uintptr_t) other_paint.p;
if (unlikely (c->current_layers.has (i)))
if (unlikely (!node.visit ((uintptr_t) other_paint.p)))
continue;
c->current_layers.add (i);
c->funcs->push_group (c->data);
c->recurse (other_paint);
c->funcs->pop_group (c->data, HB_PAINT_COMPOSITE_MODE_SRC_OVER);
c->current_layers.del (i);
}
}
break;
@ -335,18 +331,16 @@ _hb_ft_paint (hb_ft_paint_context_t *c,
{
hb_codepoint_t gid = paint.u.colr_glyph.glyphID;
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);
c->ft_font->lock.unlock ();
if (c->funcs->color_glyph (c->data, gid, c->font))
{
c->ft_font->lock.lock ();
c->funcs->pop_transform (c->data);
c->current_glyphs.del (gid);
return;
}
c->ft_font->lock.lock ();
@ -382,8 +376,6 @@ _hb_ft_paint (hb_ft_paint_context_t *c,
if (has_clip_box)
c->funcs->pop_clip (c->data);
c->current_glyphs.del (gid);
}
}
break;
@ -508,7 +500,8 @@ hb_ft_paint_glyph_colr (hb_font_t *font,
hb_ft_paint_context_t c (ft_font, font,
paint_funcs, paint_data,
palette, palette_index, foreground);
c.current_glyphs.add (gid);
hb_decycler_node_t node (c.glyphs_decycler);
node.visit (gid);
bool is_bounded = true;
FT_ClipBox clip_box;
@ -532,7 +525,8 @@ hb_ft_paint_glyph_colr (hb_font_t *font,
hb_ft_paint_context_t ce (ft_font, font,
extents_funcs, &extents_data,
palette, palette_index, foreground);
ce.current_glyphs.add (gid);
hb_decycler_node_t node2 (ce.glyphs_decycler);
node2.visit (gid);
ce.funcs->push_root_transform (ce.data, font);
ce.recurse (paint);
ce.funcs->pop_transform (ce.data);

View file

@ -131,6 +131,7 @@
#pragma GCC diagnostic ignored "-Wclass-memaccess"
#pragma GCC diagnostic ignored "-Wcast-function-type-strict" // https://github.com/harfbuzz/harfbuzz/pull/3859#issuecomment-1295409126
#pragma GCC diagnostic ignored "-Wdangling-reference" // https://github.com/harfbuzz/harfbuzz/issues/4043
#pragma GCC diagnostic ignored "-Wdangling-pointer" // Trigerred by hb_decycler_node_t().
#pragma GCC diagnostic ignored "-Wformat-nonliteral"
#pragma GCC diagnostic ignored "-Wformat-zero-length"
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"

View file

@ -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',
@ -725,6 +726,7 @@ if get_option('tests').enabled()
'test-bimap': ['test-bimap.cc', 'hb-static.cc'],
'test-cff': ['test-cff.cc', 'hb-static.cc'],
'test-classdef-graph': ['graph/test-classdef-graph.cc', 'hb-static.cc', 'graph/gsubgpos-context.cc'],
'test-decycler': ['test-decycler.cc', 'hb-static.cc'],
'test-iter': ['test-iter.cc', 'hb-static.cc'],
'test-machinery': ['test-machinery.cc', 'hb-static.cc'],
'test-map': ['test-map.cc', 'hb-static.cc'],

114
src/test-decycler.cc Normal file
View file

@ -0,0 +1,114 @@
/*
* 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
*/
#include "hb.hh"
#include "hb-decycler.hh"
static void
tree_recurse_binary (unsigned value,
unsigned max_value,
hb_decycler_t &decycler)
{
if (value >= max_value)
return;
hb_decycler_node_t node (decycler);
bool ret = node.visit (value);
assert (ret);
tree_recurse_binary (value * 2 + 1, max_value, decycler);
tree_recurse_binary (value * 2 + 2, max_value, decycler);
}
static void
tree_recurse_tertiary (unsigned value,
unsigned max_value,
hb_decycler_t &decycler)
{
/* This function implements an alternative way to use the
* decycler. It checks for each node before visiting it.
* It demonstrates reusing a node for multiple visits. */
if (value >= max_value)
return;
hb_decycler_node_t node (decycler);
value *= 3;
for (unsigned i = 1; i <= 3; i++)
{
bool ret = node.visit (value + i);
assert (ret);
tree_recurse_tertiary (value + i, max_value, decycler);
}
}
static void
test_tree ()
{
hb_decycler_t decycler;
tree_recurse_binary (0, 64, decycler);
tree_recurse_tertiary (0, 1000, decycler);
}
static void
cycle_recurse (signed value,
signed cycle_length,
hb_decycler_t &decycler)
{
assert (cycle_length > 0);
hb_decycler_node_t node (decycler);
if (!node.visit (value))
return;
if (value >= cycle_length)
value = value % cycle_length;
cycle_recurse (value + 1, cycle_length, decycler);
}
static void
test_cycle ()
{
hb_decycler_t decycler;
cycle_recurse (2, 3, decycler);
cycle_recurse (-20, 8, decycler);
}
int
main (int argc, char **argv)
{
test_tree ();
test_cycle ();
return 0;
}