@@ -31,6 +31,25 @@ use crate::graphics::bitmaps::{Bitmap, BitmapKey, BitmapWindowMut, BitmapsContai
3131use imageflow_types:: ImageInfo ;
3232use itertools:: Itertools ;
3333
34+ /// Input bytes for the zen pipeline — either an Arc-wrapped owned Vec or a static slice.
35+ /// Stored instead of `Vec<u8>` so the v2 non-zen path pays zero clone cost at registration time.
36+ /// The `Vec<u8>` zenpipe needs is built lazily in `zen_execute_inner`.
37+ #[ cfg( feature = "zen-pipeline" ) ]
38+ enum ZenInput {
39+ Owned ( Arc < Vec < u8 > > ) ,
40+ Static ( & ' static [ u8 ] ) ,
41+ }
42+
43+ #[ cfg( feature = "zen-pipeline" ) ]
44+ impl ZenInput {
45+ fn to_vec ( & self ) -> Vec < u8 > {
46+ match self {
47+ ZenInput :: Owned ( a) => ( * * a) . clone ( ) ,
48+ ZenInput :: Static ( s) => s. to_vec ( ) ,
49+ }
50+ }
51+ }
52+
3453/// Something of a god object (which is necessary for a reasonable FFI interface).
3554/// 1025 bytes including 5 heap allocations as of Oct 2025. If on the stack, 312 bytes are taken up
3655pub struct Context {
@@ -60,8 +79,9 @@ pub struct Context {
6079 /// Input bytes stashed for the zen pipeline (feature-gated).
6180 /// Populated by `add_copied_input_buffer`, `add_input_vector`, etc.
6281 /// The zen pipeline reads from here instead of the codec containers.
82+ /// Uses Arc to avoid cloning on the v2 non-zen path — Vec is built lazily at execute time.
6383 #[ cfg( feature = "zen-pipeline" ) ]
64- zen_input_bytes : std:: collections:: HashMap < i32 , Vec < u8 > > ,
84+ zen_input_bytes : std:: collections:: HashMap < i32 , ZenInput > ,
6585
6686 /// Force a specific backend for testing. None = zen when zen-pipeline is compiled in.
6787 #[ cfg( feature = "zen-pipeline" ) ]
@@ -578,17 +598,31 @@ impl Context {
578598
579599 pub fn add_copied_input_buffer ( & mut self , io_id : i32 , bytes : & [ u8 ] ) -> Result < ( ) > {
580600 #[ cfg( feature = "zen-pipeline" ) ]
581- self . zen_input_bytes . insert ( io_id, bytes. to_vec ( ) ) ;
582-
583- let io = IoProxy :: copy_slice ( self , io_id, bytes) . map_err ( |e| e. at ( here ! ( ) ) ) ?;
584- self . add_io ( io, io_id, IoDirection :: In ) . map_err ( |e| e. at ( here ! ( ) ) )
601+ {
602+ let arc = Arc :: new ( bytes. to_vec ( ) ) ;
603+ self . zen_input_bytes . insert ( io_id, ZenInput :: Owned ( arc. clone ( ) ) ) ;
604+ let io = IoProxy :: read_arc ( self , io_id, arc) . map_err ( |e| e. at ( here ! ( ) ) ) ?;
605+ return self . add_io ( io, io_id, IoDirection :: In ) . map_err ( |e| e. at ( here ! ( ) ) ) ;
606+ }
607+ #[ cfg( not( feature = "zen-pipeline" ) ) ]
608+ {
609+ let io = IoProxy :: copy_slice ( self , io_id, bytes) . map_err ( |e| e. at ( here ! ( ) ) ) ?;
610+ self . add_io ( io, io_id, IoDirection :: In ) . map_err ( |e| e. at ( here ! ( ) ) )
611+ }
585612 }
586613 pub fn add_input_vector ( & mut self , io_id : i32 , bytes : Vec < u8 > ) -> Result < ( ) > {
587614 #[ cfg( feature = "zen-pipeline" ) ]
588- self . zen_input_bytes . insert ( io_id, bytes. clone ( ) ) ;
589-
590- let io = IoProxy :: read_vec ( self , io_id, bytes) . map_err ( |e| e. at ( here ! ( ) ) ) ?;
591- self . add_io ( io, io_id, IoDirection :: In ) . map_err ( |e| e. at ( here ! ( ) ) )
615+ {
616+ let arc = Arc :: new ( bytes) ;
617+ self . zen_input_bytes . insert ( io_id, ZenInput :: Owned ( arc. clone ( ) ) ) ;
618+ let io = IoProxy :: read_arc ( self , io_id, arc) . map_err ( |e| e. at ( here ! ( ) ) ) ?;
619+ return self . add_io ( io, io_id, IoDirection :: In ) . map_err ( |e| e. at ( here ! ( ) ) ) ;
620+ }
621+ #[ cfg( not( feature = "zen-pipeline" ) ) ]
622+ {
623+ let io = IoProxy :: read_vec ( self , io_id, bytes) . map_err ( |e| e. at ( here ! ( ) ) ) ?;
624+ self . add_io ( io, io_id, IoDirection :: In ) . map_err ( |e| e. at ( here ! ( ) ) )
625+ }
592626 }
593627
594628 /// Zero-copy: borrows `bytes` without copying.
@@ -602,7 +636,7 @@ impl Context {
602636 /// In practice, the ABI layer (imageflow_abi) uses transmute to erase the real lifetime.
603637 pub fn add_input_buffer ( & mut self , io_id : i32 , bytes : & ' static [ u8 ] ) -> Result < ( ) > {
604638 #[ cfg( feature = "zen-pipeline" ) ]
605- self . zen_input_bytes . insert ( io_id, bytes . to_vec ( ) ) ;
639+ self . zen_input_bytes . insert ( io_id, ZenInput :: Static ( bytes ) ) ;
606640
607641 let io = IoProxy :: read_slice ( self , io_id, bytes) . map_err ( |e| e. at ( here ! ( ) ) ) ?;
608642 self . add_io ( io, io_id, IoDirection :: In ) . map_err ( |e| e. at ( here ! ( ) ) )
@@ -875,8 +909,11 @@ impl Context {
875909 }
876910 let job_options = what. job_options . unwrap_or_default ( ) ;
877911
912+ let io_bytes: std:: collections:: HashMap < i32 , Vec < u8 > > =
913+ self . zen_input_bytes . iter ( ) . map ( |( & id, z) | ( id, z. to_vec ( ) ) ) . collect ( ) ;
914+
878915 let output =
879- crate :: zen:: zen_execute ( & what. framewise , & self . zen_input_bytes , & self . security , & job_options)
916+ crate :: zen:: zen_execute ( & what. framewise , & io_bytes , & self . security , & job_options)
880917 . map_err ( |e| e. at ( here ! ( ) ) ) ?;
881918
882919 // Store encoded outputs in Context's output buffer system.
0 commit comments