[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.
This commit is contained in:
Behdad Esfahbod 2025-02-15 23:19:44 -07:00
parent ed76c8559e
commit 0aa400b1d8
3 changed files with 112 additions and 15 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;
@ -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 */

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

@ -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 */

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',