@@ -253,8 +253,16 @@ impl ProcessorNode for CompositorNode {
253253
254254 // Decode image overlays (once). Wrap in Arc so per-frame clones
255255 // into the work item are cheap reference-count bumps.
256+ //
257+ // `image_overlay_cfg_indices` records, for each successfully decoded
258+ // overlay, the index of the originating `ImageOverlayConfig` in
259+ // `config.image_overlays`. This allows the cache in
260+ // `apply_update_params` to map decoded bitmaps back to their configs
261+ // without relying on dimension-matching heuristics.
256262 let mut image_overlays_vec: Vec < Arc < DecodedOverlay > > =
257263 Vec :: with_capacity ( self . config . image_overlays . len ( ) ) ;
264+ let mut image_overlay_cfg_indices: Vec < usize > =
265+ Vec :: with_capacity ( self . config . image_overlays . len ( ) ) ;
258266 for ( i, img_cfg) in self . config . image_overlays . iter ( ) . enumerate ( ) {
259267 match decode_image_overlay ( img_cfg) {
260268 Ok ( overlay) => {
@@ -269,6 +277,7 @@ impl ProcessorNode for CompositorNode {
269277 overlay. rect. height,
270278 ) ;
271279 image_overlays_vec. push ( Arc :: new ( overlay) ) ;
280+ image_overlay_cfg_indices. push ( i) ;
272281 } ,
273282 Err ( e) => {
274283 tracing:: warn!( "Failed to decode image overlay {}: {}" , i, e) ;
@@ -409,6 +418,7 @@ impl ProcessorNode for CompositorNode {
409418 Self :: apply_update_params(
410419 & mut self . config,
411420 & mut image_overlays,
421+ & mut image_overlay_cfg_indices,
412422 & mut text_overlays,
413423 params,
414424 & mut stats_tracker,
@@ -490,6 +500,7 @@ impl ProcessorNode for CompositorNode {
490500 Self :: apply_update_params(
491501 & mut self . config,
492502 & mut image_overlays,
503+ & mut image_overlay_cfg_indices,
493504 & mut text_overlays,
494505 params,
495506 & mut stats_tracker,
@@ -530,6 +541,7 @@ impl ProcessorNode for CompositorNode {
530541 Self :: apply_update_params (
531542 & mut self . config ,
532543 & mut image_overlays,
544+ & mut image_overlay_cfg_indices,
533545 & mut text_overlays,
534546 params,
535547 & mut stats_tracker,
@@ -652,6 +664,7 @@ impl CompositorNode {
652664 fn apply_update_params (
653665 config : & mut CompositorConfig ,
654666 image_overlays : & mut Arc < [ Arc < DecodedOverlay > ] > ,
667+ image_overlay_cfg_indices : & mut Vec < usize > ,
655668 text_overlays : & mut Arc < [ Arc < DecodedOverlay > ] > ,
656669 params : serde_json:: Value ,
657670 stats_tracker : & mut NodeStatsTracker ,
@@ -673,76 +686,71 @@ impl CompositorNode {
673686 // bitmaps are reused via Arc, avoiding redundant base64
674687 // decode + bilinear prescale work.
675688 //
676- // We match cached overlays by content identity rather
677- // than positional index because failed decodes cause the
678- // decoded slice to be shorter than the config vec,
679- // making positional lookups incorrect.
689+ // The cache is keyed by (data_base64, width, height).
690+ // `image_overlay_cfg_indices` provides an exact mapping
691+ // from each decoded overlay back to its originating
692+ // config index, eliminating any heuristic guessing
693+ // about which decoded bitmap belongs to which config.
680694 let old_imgs = image_overlays. clone ( ) ;
681695 let old_cfgs = & config. image_overlays ;
682696
683- // Build a content-keyed cache from the successfully
684- // decoded old overlays. We match each decoded bitmap
685- // back to its originating config by walking old_cfgs
686- // and old_imgs together: for each old config we check
687- // whether the next decoded overlay's dimensions match
688- // the config's target rect (a successful decode
689- // prescales to exactly those dimensions). On mismatch
690- // we skip the config (it must have been a failed
691- // decode) without consuming a decoded entry.
692- //
693- // The cache key is (data_base64, width, height) and
694- // values are Vec to handle duplicate images at the
695- // same size.
696- type CacheKey < ' a > = ( & ' a str , u32 , u32 ) ;
697- let mut cache: HashMap < CacheKey < ' _ > , Vec < Arc < DecodedOverlay > > > = HashMap :: new ( ) ;
698- let mut dec_iter = old_imgs. iter ( ) . peekable ( ) ;
699- for old_cfg in old_cfgs. iter ( ) {
700- if let Some ( decoded) = dec_iter. peek ( ) {
701- let tw = old_cfg. transform . rect . width ;
702- let th = old_cfg. transform . rect . height ;
703- // A successfully decoded overlay is prescaled
704- // to the config's target dimensions. If the
705- // next decoded bitmap's size matches we know
706- // it belongs to this config; otherwise this
707- // config's decode must have failed.
708- if decoded. width == tw && decoded. height == th {
709- let key: CacheKey < ' _ > = ( & old_cfg. data_base64 , tw, th) ;
710- cache
711- . entry ( key)
712- . or_default ( )
713- . push ( Arc :: clone ( dec_iter. next ( ) . expect ( "peeked" ) ) ) ;
697+ let mut cache: HashMap < ( & str , u32 , u32 ) , Vec < Arc < DecodedOverlay > > > =
698+ HashMap :: new ( ) ;
699+
700+ // Each decoded overlay has a recorded config index in
701+ // `image_overlay_cfg_indices`. Use this to look up
702+ // the originating config directly — no dimension
703+ // matching needed.
704+ for ( dec_idx, decoded) in old_imgs. iter ( ) . enumerate ( ) {
705+ if let Some ( & cfg_idx) = image_overlay_cfg_indices. get ( dec_idx) {
706+ if let Some ( old_cfg) = old_cfgs. get ( cfg_idx) {
707+ let key = (
708+ old_cfg. data_base64 . as_str ( ) ,
709+ old_cfg. transform . rect . width ,
710+ old_cfg. transform . rect . height ,
711+ ) ;
712+ cache. entry ( key) . or_default ( ) . push ( Arc :: clone ( decoded) ) ;
714713 }
715714 }
716715 }
717716
718717 let mut new_image_overlays: Vec < Arc < DecodedOverlay > > =
719718 Vec :: with_capacity ( new_config. image_overlays . len ( ) ) ;
720- for img_cfg in & new_config. image_overlays {
721- let key: CacheKey < ' _ > = (
722- & img_cfg. data_base64 ,
719+ let mut new_cfg_indices: Vec < usize > =
720+ Vec :: with_capacity ( new_config. image_overlays . len ( ) ) ;
721+ for ( new_idx, img_cfg) in new_config. image_overlays . iter ( ) . enumerate ( ) {
722+ let key = (
723+ img_cfg. data_base64 . as_str ( ) ,
723724 img_cfg. transform . rect . width ,
724725 img_cfg. transform . rect . height ,
725726 ) ;
726727 if let Some ( entries) = cache. get_mut ( & key) {
727728 if let Some ( existing) = entries. pop ( ) {
728729 // Content and target dimensions unchanged —
729- // reuse the decoded bitmap, just update
730- // mutable transform fields.
730+ // reuse the decoded bitmap. The cache key
731+ // already guarantees width/height match, so
732+ // only position and visual fields can differ.
731733 let mut ov = ( * existing) . clone ( ) ;
732- ov. rect = img_cfg. transform . rect . clone ( ) ;
734+ ov. rect . x = img_cfg. transform . rect . x ;
735+ ov. rect . y = img_cfg. transform . rect . y ;
733736 ov. opacity = img_cfg. transform . opacity ;
734737 ov. rotation_degrees = img_cfg. transform . rotation_degrees ;
735738 ov. z_index = img_cfg. transform . z_index ;
736739 new_image_overlays. push ( Arc :: new ( ov) ) ;
740+ new_cfg_indices. push ( new_idx) ;
737741 continue ;
738742 }
739743 }
740744 match decode_image_overlay ( img_cfg) {
741- Ok ( ov) => new_image_overlays. push ( Arc :: new ( ov) ) ,
745+ Ok ( ov) => {
746+ new_image_overlays. push ( Arc :: new ( ov) ) ;
747+ new_cfg_indices. push ( new_idx) ;
748+ } ,
742749 Err ( e) => tracing:: warn!( "Image overlay decode failed: {e}" ) ,
743750 }
744751 }
745752 * image_overlays = Arc :: from ( new_image_overlays) ;
753+ * image_overlay_cfg_indices = new_cfg_indices;
746754
747755 // Re-rasterize text overlays.
748756 let new_text_overlays: Vec < Arc < DecodedOverlay > > = new_config
0 commit comments