This is an automated email from the git hooks/post-receive script.
git pushed a commit to branch edje-vector-intergration
in repository efl.
View the commit online.
commit 44179251cbdb990aa53579bf1f1449afbf5ff916
Author: [email protected] <[email protected]>
AuthorDate: Wed Apr 29 14:23:35 2026 -0600
edje: cache materialized Efl_VG during vector transitions (Phase 5.5)
Add Edje_Vg_Cache_Slot to Edje_Real_Part_Vector with desc_key, working_tree,
and efl_vg fields. Two cache slots (cache_chosen, cache_param2) cover the
two descriptions a transition can have active.
Cache-or-build helper _edje_vg_cache_get_or_build validates on desc_key
match and classes_dirty flag; cache miss clears and rebuilds. Per-frame cost
during transitions drops from 3 materializations to 1 (only the lerp tree).
Static rendering uses efl_duplicate for ref-counted canvas ownership.
_edje_vg_cache_slot_clear handles teardown; lifetime invariant matches
Phase 1.2 (cached_base_tree): the part is torn down before cache's desc_key
pointee is freed. Phase 6 will wire classes_dirty in the color-class
observer callback.
Unit test edje_vg_cache_hit_on_same_desc validates hit/miss transitions,
classes_dirty invalidation, and idempotent clear.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---
src/lib/edje/edje_calc.c | 134 ++++++++++++++-----------------
src/lib/edje/edje_load.c | 5 ++
src/lib/edje/edje_private.h | 28 +++++++
src/lib/edje/edje_vg_tree.c | 75 +++++++++++++++++
src/lib/edje/edje_vg_tree.h | 23 ++++++
src/tests/edje/edje_test_vector_states.c | 80 ++++++++++++++++++
6 files changed, 271 insertions(+), 74 deletions(-)
diff --git a/src/lib/edje/edje_calc.c b/src/lib/edje/edje_calc.c
index 3965d49703..07b03be821 100644
--- a/src/lib/edje/edje_calc.c
+++ b/src/lib/edje/edje_calc.c
@@ -3282,15 +3282,18 @@ _edje_vector_recalc_apply(Edje *ed, Edje_Real_Part *ep, Edje_Calc_Params *p3 EIN
when tree_id == -1 (its initialised-invalid value). */
if (chosen_desc->vg.tree_id >= 0)
{
+ /* Phase 5.5: cache-first materialization. We maintain one cache slot
+ per description side (chosen and param2). On a hit the slot returns
+ its already-built working_tree and efl_vg without any allocation.
+ Only the lerp output (which changes every frame because pos changes)
+ is rebuilt from scratch. In the static case we efl_duplicate the
+ cached efl_vg, which is much cheaper than a full tree walk. */
Edje_Real_Part_Vector *rpv = ep->typedata.vector;
const Edje_Vg_Tree *base_a;
- Edje_Vg_Tree *working_a = NULL;
- Edje_Vg_Tree *working_b = NULL;
+ Efl_VG *efl_a = NULL, *efl_b = NULL, *root = NULL;
+ const Edje_Vg_Tree *working_a = NULL, *working_b = NULL;
Edje_Vg_Tree *lerp_tree = NULL;
- Efl_VG *efl_a = NULL, *efl_b = NULL;
Eina_Rect viewbox;
- Eina_List *l;
- Edje_Vg_Override *ovr;
if (!ed->file->vector_dir ||
(unsigned int)chosen_desc->vg.tree_id >= ed->file->vector_dir->trees_count)
@@ -3303,34 +3306,23 @@ _edje_vector_recalc_apply(Edje *ed, Edje_Real_Part *ep, Edje_Calc_Params *p3 EIN
base_a = &ed->file->vector_dir->trees[chosen_desc->vg.tree_id];
- /* Cache the tree pointer; useful for class-dirty invalidation in
- Phase 6. */
+ /* Keep the Phase 4 borrowed pointer up-to-date (Phase 6 uses it). */
rpv->cached_base_tree = base_a;
- rpv->cached_tree_id = chosen_desc->vg.tree_id;
+ rpv->cached_tree_id = chosen_desc->vg.tree_id;
- /* Phase 4: when per-description overrides are present, dup the base
- tree, apply each override in order. When there are no overrides,
- skip the dup and use the immutable base tree directly (fast path). */
- if (chosen_desc->vg.overrides)
+ /* Build or fetch the chosen description's cache slot (A side). */
+ if (!_edje_vg_cache_get_or_build(&rpv->cache_chosen,
+ chosen_desc, base_a,
+ chosen_desc->vg.overrides,
+ rpv->classes_dirty,
+ &efl_a, &working_a))
{
- working_a = _edje_vg_tree_dup(base_a);
- if (!working_a)
- {
- ERR("Failed to dup base tree for override application (tree_id=%d)",
- chosen_desc->vg.tree_id);
- return;
- }
- EINA_LIST_FOREACH(chosen_desc->vg.overrides, l, ovr)
- _edje_vg_tree_apply_override(working_a, ovr);
+ ERR("Failed to build cache for chosen description (tree_id=%d)",
+ chosen_desc->vg.tree_id);
+ return;
}
- const Edje_Vg_Tree *src_a = working_a ? working_a : base_a;
- /* Phase 5: when a transition is active (param2 non-NULL and pos > 0),
- try to build a lerp tree across both descriptions. If the next
- description is not tree-based (tree_id < 0, e.g. Mode-C SVG), fall
- through to the direct-materialise path instead; Mode-C bridging is
- deferred. */
- root = NULL;
+ /* Transition active? (param2 set and pos meaningfully non-zero) */
if (ep->param2 && NEQ(pos, ZERO))
{
Edje_Part_Description_Vector *next_desc =
@@ -3342,67 +3334,61 @@ _edje_vector_recalc_apply(Edje *ed, Edje_Real_Part *ep, Edje_Calc_Params *p3 EIN
const Edje_Vg_Tree *base_b =
&ed->file->vector_dir->trees[next_desc->vg.tree_id];
- /* Build B's working tree (dup + apply overrides if any). */
- if (next_desc->vg.overrides)
+ /* Build or fetch the param2 description's cache slot (B side). */
+ if (_edje_vg_cache_get_or_build(&rpv->cache_param2,
+ next_desc, base_b,
+ next_desc->vg.overrides,
+ rpv->classes_dirty,
+ &efl_b, &working_b))
{
- working_b = _edje_vg_tree_dup(base_b);
- if (working_b)
+ /* Lerp tree: fresh every frame because pos changes.
+ This is the only materialization that fires per-frame
+ during a transition (efl_a and efl_b are cache hits). */
+ lerp_tree = _edje_vg_tree_lerp(working_a, working_b,
+ TO_DOUBLE(pos));
+ if (lerp_tree)
{
- EINA_LIST_FOREACH(next_desc->vg.overrides, l, ovr)
- _edje_vg_tree_apply_override(working_b, ovr);
+ root = _edje_vg_tree_to_efl_vg(lerp_tree);
+ /* Path-only interpolation across A/B cached VGs.
+ Returns EINA_FALSE on topology mismatch — root
+ already holds B paths (step semantics), so
+ failure is safe to ignore here. */
+ if (root && efl_a && efl_b)
+ efl_gfx_path_interpolate(root, efl_a, efl_b,
+ TO_DOUBLE(pos));
}
}
- const Edje_Vg_Tree *src_b = working_b ? working_b : base_b;
-
- /* Build the lerp tree: structure mirrors B with non-path
- properties lerped from A. Paths stay at B's value here;
- efl_gfx_path_interpolate handles them below. */
- lerp_tree = _edje_vg_tree_lerp(src_a, src_b, TO_DOUBLE(pos));
-
- /* Materialise all three trees. efl_a and efl_b are
- short-lived references used only for path interpolation. */
- efl_a = _edje_vg_tree_to_efl_vg(src_a);
- efl_b = _edje_vg_tree_to_efl_vg(src_b);
- if (lerp_tree) root = _edje_vg_tree_to_efl_vg(lerp_tree);
-
- /* Path-only interpolation: walks the Efl_VG hierarchy and
- updates each shape's path in `root` via the EFL-native
- interpolator. Returns EINA_FALSE on topology mismatch —
- root already holds B's paths (step semantics), so failure
- is safe to ignore. */
- if (root && efl_a && efl_b)
- efl_gfx_path_interpolate(root, efl_a, efl_b, TO_DOUBLE(pos));
-
- /* Release auxiliary A/B VG trees; root is independent. */
- if (efl_a) efl_unref(efl_a);
- if (efl_b) efl_unref(efl_b);
}
}
- /* No transition active, transition fell through (Mode-C next desc),
- or lerp_tree materialise failed: fall back to direct src_a render. */
+ /* Static fallback: no transition active, transition fell through
+ (Mode-C next desc), or lerp_tree materialization failed.
+ efl_duplicate is cheaper than re-walking the data model because
+ it copies the already-built Efl_VG object graph. */
if (!root)
- root = _edje_vg_tree_to_efl_vg(src_a);
+ {
+ if (efl_a)
+ root = efl_duplicate(efl_a);
+ /* efl_a is still cached in the slot; root is an independent copy. */
+ }
- /* Viewbox: use lerped viewbox when transition is active (the lerp tree
- already has the interpolated vbx/vby/vbw/vbh); else use base_a. */
+ /* Viewbox from lerp tree when transitioning, else from working_a. */
if (lerp_tree)
viewbox = EINA_RECT(lerp_tree->vbx, lerp_tree->vby,
lerp_tree->vbw, lerp_tree->vbh);
+ else if (working_a)
+ viewbox = EINA_RECT(working_a->vbx, working_a->vby,
+ working_a->vbw, working_a->vbh);
else
viewbox = EINA_RECT(base_a->vbx, base_a->vby,
base_a->vbw, base_a->vbh);
- /* TODO Phase 5+: cache materialised Efl_VG hierarchies to avoid the
- dup-and-walk cost on every recalc. During a transition we currently
- build three Efl_VG instances per frame (A, B, and OUT). At 60fps
- this is 180 instantiations/sec for moderately-sized trees. Cache
- key: (description pointer, override-list-hash, classes_dirty). */
-
- /* Free working trees and the lerp tree; root is the only survivor. */
+ /* lerp_tree is a per-frame allocation — always free it. */
if (lerp_tree) _edje_vg_tree_free(lerp_tree);
- if (working_a) _edje_vg_tree_free(working_a);
- if (working_b) _edje_vg_tree_free(working_b);
+
+ /* Clear classes_dirty now that we've consumed it: any stale class
+ data has been flushed by the cache rebuild triggered above. */
+ rpv->classes_dirty = 0;
if (!root)
{
@@ -3413,8 +3399,8 @@ _edje_vector_recalc_apply(Edje *ed, Edje_Real_Part *ep, Edje_Calc_Params *p3 EIN
efl_canvas_vg_object_viewbox_set(ep->object, viewbox);
efl_canvas_vg_object_root_node_set(ep->object, root);
/* root_node_set takes its own ref via efl_replace; release our
- caller-side ref from efl_add_ref so the tree is owned exclusively
- by the VG object from this point forward. */
+ caller-side ref (from efl_add_ref / efl_duplicate) so the tree is
+ owned exclusively by the VG object from this point forward. */
efl_unref(root);
return;
}
diff --git a/src/lib/edje/edje_load.c b/src/lib/edje/edje_load.c
index 972a5f2f38..83aa87daff 100644
--- a/src/lib/edje/edje_load.c
+++ b/src/lib/edje/edje_load.c
@@ -2019,6 +2019,11 @@ _edje_file_del(Edje *ed)
if (rp->typedata.vector->lottie_data)
free(rp->typedata.vector->lottie_data);
+ /* Phase 5.5: release any cached working trees and Efl_VG
+ objects before freeing the containing struct. */
+ _edje_vg_cache_slot_clear(&rp->typedata.vector->cache_chosen);
+ _edje_vg_cache_slot_clear(&rp->typedata.vector->cache_param2);
+
free(rp->typedata.vector);
rp->typedata.vector = NULL;
}
diff --git a/src/lib/edje/edje_private.h b/src/lib/edje/edje_private.h
index 261eff8bb6..4b3c81c3c7 100644
--- a/src/lib/edje/edje_private.h
+++ b/src/lib/edje/edje_private.h
@@ -2024,6 +2024,27 @@ struct _Edje_Real_Part_Swallow
} swallow_params; // 28 // FIXME: only if type SWALLOW
};
+/* Phase 5.5 cache — per-description materialized output. Each slot
+ carries the dup'd-and-overrides-applied Edje_Vg_Tree plus its
+ materialized Efl_VG. The slot is keyed by description pointer
+ (Edje_Part_Description_Vector*). Invalidation triggers:
+ - desc_key changes (different chosen/param2 description landed).
+ - classes_dirty is set (Phase 6 wires this via the color-class
+ observer callback).
+ On invalidation: efl_unref(efl_vg), _edje_vg_tree_free(working_tree).
+ Both slots' lifetimes are bounded by Edje_Real_Part_Vector — freed
+ at part teardown in edje_load.c.
+
+ working_tree is owned (calloc'd via _edje_vg_tree_dup).
+ efl_vg holds an owning ref (from _edje_vg_tree_to_efl_vg's
+ efl_add_ref). desc_key is BORROWED (non-owning pointer). */
+typedef struct _Edje_Vg_Cache_Slot
+{
+ const void *desc_key; /* Edje_Part_Description_Vector* */
+ Edje_Vg_Tree *working_tree;
+ Efl_VG *efl_vg;
+} Edje_Vg_Cache_Slot;
+
struct _Edje_Real_Part_Vector
{
Eo *anim;
@@ -2047,6 +2068,13 @@ struct _Edje_Real_Part_Vector
const Edje_Vg_Tree *cached_base_tree;
int cached_tree_id;
Eina_Bool classes_dirty : 1;
+ /* Phase 5.5: per-description materialized-Efl_VG cache slots.
+ cache_chosen is keyed on the chosen description (param1 side).
+ cache_param2 is keyed on the transition target (param2 side).
+ Both are zero-init'd by the calloc in edje_load.c so desc_key==NULL
+ means empty on construction. */
+ Edje_Vg_Cache_Slot cache_chosen;
+ Edje_Vg_Cache_Slot cache_param2;
};
struct _Edje_Real_Part
diff --git a/src/lib/edje/edje_vg_tree.c b/src/lib/edje/edje_vg_tree.c
index a4781a3350..cf83cff73b 100644
--- a/src/lib/edje/edje_vg_tree.c
+++ b/src/lib/edje/edje_vg_tree.c
@@ -1705,3 +1705,78 @@ _edje_vg_tree_to_efl_vg(const Edje_Vg_Tree *t)
return root;
}
+
+/* =========================================================================
+ * Phase 5.5: materialized-Efl_VG cache helpers
+ * ========================================================================= */
+
+EAPI void
+_edje_vg_cache_slot_clear(Edje_Vg_Cache_Slot *slot)
+{
+ if (!slot) return;
+ if (slot->efl_vg)
+ {
+ efl_unref(slot->efl_vg);
+ slot->efl_vg = NULL;
+ }
+ if (slot->working_tree)
+ {
+ _edje_vg_tree_free(slot->working_tree);
+ slot->working_tree = NULL;
+ }
+ slot->desc_key = NULL;
+}
+
+EAPI Eina_Bool
+_edje_vg_cache_get_or_build(Edje_Vg_Cache_Slot *slot,
+ const void *desc_key,
+ const Edje_Vg_Tree *base,
+ const Eina_List *overrides,
+ Eina_Bool classes_dirty_in,
+ Efl_VG **out_efl_vg,
+ const Edje_Vg_Tree **out_working_tree)
+{
+ if (!slot || !desc_key || !base) return EINA_FALSE;
+
+ /* Cache hit: same description and classes not dirty. */
+ if (slot->desc_key == desc_key &&
+ !classes_dirty_in &&
+ slot->efl_vg && slot->working_tree)
+ {
+ if (out_efl_vg) *out_efl_vg = slot->efl_vg;
+ if (out_working_tree) *out_working_tree = slot->working_tree;
+ return EINA_TRUE;
+ }
+
+ /* Cache miss or invalidation: rebuild. */
+ _edje_vg_cache_slot_clear(slot);
+
+ /* Always dup the base tree so the slot owns an independent working tree.
+ This simplifies lifetime management — the slot's working_tree is never
+ an alias into the file's immutable base array. */
+ Edje_Vg_Tree *working = _edje_vg_tree_dup(base);
+ if (!working) return EINA_FALSE;
+
+ if (overrides)
+ {
+ const Eina_List *l;
+ const Edje_Vg_Override *ovr;
+ EINA_LIST_FOREACH(overrides, l, ovr)
+ _edje_vg_tree_apply_override(working, ovr);
+ }
+
+ Efl_VG *efl_vg = _edje_vg_tree_to_efl_vg(working);
+ if (!efl_vg)
+ {
+ _edje_vg_tree_free(working);
+ return EINA_FALSE;
+ }
+
+ slot->desc_key = desc_key;
+ slot->working_tree = working;
+ slot->efl_vg = efl_vg;
+
+ if (out_efl_vg) *out_efl_vg = efl_vg;
+ if (out_working_tree) *out_working_tree = working;
+ return EINA_TRUE;
+}
diff --git a/src/lib/edje/edje_vg_tree.h b/src/lib/edje/edje_vg_tree.h
index 5db65173ba..eb57f2d1b8 100644
--- a/src/lib/edje/edje_vg_tree.h
+++ b/src/lib/edje/edje_vg_tree.h
@@ -152,6 +152,29 @@ EAPI Edje_Vg_Tree *_edje_vg_tree_lerp(const Edje_Vg_Tree *a,
const Edje_Vg_Tree *b,
double pos);
+/* --- Phase 5.5: materialized-Efl_VG cache helpers ----------------------- */
+
+/* Cache-or-build: if the slot's desc_key matches desc_key and classes_dirty_in
+ is clear, return the cached efl_vg and working_tree (cache hit). Otherwise
+ invalidate the slot, dup base, apply overrides, materialise a fresh Efl_VG,
+ store in slot, and return (cache miss / rebuild).
+ Returned efl_vg and working_tree are BORROWED from the slot — do not unref
+ or free them; the slot owns both.
+ Returns EINA_TRUE on success (hit or rebuild), EINA_FALSE on OOM or bad args.
+ out_efl_vg and out_working_tree may each be NULL if the caller does not need
+ them. */
+EAPI Eina_Bool _edje_vg_cache_get_or_build(Edje_Vg_Cache_Slot *slot,
+ const void *desc_key,
+ const Edje_Vg_Tree *base,
+ const Eina_List *overrides,
+ Eina_Bool classes_dirty_in,
+ Efl_VG **out_efl_vg,
+ const Edje_Vg_Tree **out_working_tree);
+
+/* Release the slot's owned contents (efl_unref + tree free) and reset
+ desc_key to NULL. Safe to call on an already-cleared or zero-init'd slot. */
+EAPI void _edje_vg_cache_slot_clear(Edje_Vg_Cache_Slot *slot);
+
/* --- Eet descriptor accessor -------------------------------------------- */
/* Returns the static Eet_Data_Descriptor for Edje_Vg_Tree.
diff --git a/src/tests/edje/edje_test_vector_states.c b/src/tests/edje/edje_test_vector_states.c
index 2b9593c2de..1ec678299c 100644
--- a/src/tests/edje/edje_test_vector_states.c
+++ b/src/tests/edje/edje_test_vector_states.c
@@ -118,6 +118,26 @@ Edje_Vg_Tree *_edje_vg_tree_lerp(const Edje_Vg_Tree *a,
const Edje_Vg_Tree *b,
double pos);
+/* Phase 5.5 cache slot — must match layout of Edje_Vg_Cache_Slot in
+ * edje_private.h exactly (three pointer-width fields). */
+typedef struct
+{
+ const void *desc_key;
+ Edje_Vg_Tree *working_tree;
+ Efl_VG *efl_vg;
+} Test_Vg_Cache_Slot;
+
+/* The cache helpers take Edje_Vg_Cache_Slot* from the library's perspective,
+ * but since both structs have identical layout, we cast freely. */
+Eina_Bool _edje_vg_cache_get_or_build(void *slot,
+ const void *desc_key,
+ const Edje_Vg_Tree *base,
+ const void *overrides,
+ Eina_Bool classes_dirty_in,
+ Efl_VG **out_efl_vg,
+ const Edje_Vg_Tree **out_working_tree);
+void _edje_vg_cache_slot_clear(void *slot);
+
/* Partial view of Edje_Vg_Override: all fields, inline payload shape sub-struct.
* We replicate only the fields needed by the three Task 4.1 tests.
* The payload is a full Edje_Vg_Node embedded (not a pointer), so we mirror
@@ -1420,6 +1440,64 @@ EFL_START_TEST(edje_vg_render_box_a_transition)
}
EFL_END_TEST
+/* Phase 5.5 — direct unit test for _edje_vg_cache_get_or_build /
+ * _edje_vg_cache_slot_clear. We build a small tree, then:
+ * a) verify cache hit returns the same efl_vg pointer on repeated calls;
+ * b) verify cache miss on a different desc_key yields a new efl_vg pointer;
+ * c) verify classes_dirty forces a rebuild even with the same desc_key;
+ * d) verify _edje_vg_cache_slot_clear resets everything without crashing on
+ * an already-clear slot.
+ */
+EFL_START_TEST(edje_vg_cache_hit_on_same_desc)
+{
+ Test_Vg_Cache_Slot slot = { NULL, NULL, NULL };
+ Edje_Vg_Tree *base = _edje_vg_tree_new_for_test();
+ fail_if(!base);
+
+ /* Sentinel pointer values as desc_key stand-ins — no actual struct needed. */
+ void *key_a = (void *)0x1234;
+ void *key_b = (void *)0x5678;
+
+ Efl_VG *vg1 = NULL;
+ const Edje_Vg_Tree *wt1 = NULL;
+
+ /* First call: cold miss — must build and return valid objects. */
+ fail_if(!_edje_vg_cache_get_or_build(&slot, key_a, base, NULL,
+ EINA_FALSE, &vg1, &wt1));
+ fail_if(!vg1);
+ fail_if(!wt1);
+
+ Efl_VG *vg2 = NULL;
+ const Edje_Vg_Tree *wt2 = NULL;
+
+ /* Second call with same key: cache hit — same pointer returned. */
+ fail_if(!_edje_vg_cache_get_or_build(&slot, key_a, base, NULL,
+ EINA_FALSE, &vg2, &wt2));
+ ck_assert_ptr_eq(vg1, vg2);
+ ck_assert_ptr_eq(wt1, wt2);
+
+ Efl_VG *vg3 = NULL;
+
+ /* Third call with different key: cache miss — fresh object. */
+ fail_if(!_edje_vg_cache_get_or_build(&slot, key_b, base, NULL,
+ EINA_FALSE, &vg3, NULL));
+ fail_if(vg3 == vg1);
+
+ Efl_VG *vg4 = NULL;
+
+ /* Fourth call: same key_b but classes_dirty forces rebuild. */
+ fail_if(!_edje_vg_cache_get_or_build(&slot, key_b, base, NULL,
+ EINA_TRUE, &vg4, NULL));
+ fail_if(vg4 == vg3);
+
+ /* Explicit clear: must not crash even when called twice (idempotent). */
+ _edje_vg_cache_slot_clear(&slot);
+ _edje_vg_cache_slot_clear(&slot);
+
+ _edje_vg_tree_free(base);
+}
+EFL_END_TEST
+
void
edje_test_vector_states(TCase *tc)
{
@@ -1447,4 +1525,6 @@ edje_test_vector_states(TCase *tc)
tcase_add_test(tc, edje_vg_tree_lerp_basic);
/* Phase 5.4 end-to-end transition */
tcase_add_test(tc, edje_vg_render_box_a_transition);
+ /* Phase 5.5 materialized-Efl_VG cache */
+ tcase_add_test(tc, edje_vg_cache_hit_on_same_desc);
}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.