@@ -104,6 +104,16 @@ enum PendingEvent {
104104 } ,
105105}
106106
107+ /// A GPU→CPU pixel readback still in flight (see `WgpuCanvas::begin_read_pixels`).
108+ struct PendingSnap {
109+ buffer : wgpu:: Buffer ,
110+ rx : std:: sync:: mpsc:: Receiver < Result < ( ) , wgpu:: BufferAsyncError > > ,
111+ submission : wgpu:: SubmissionIndex ,
112+ width : usize ,
113+ height : usize ,
114+ padded_bytes_per_row : usize ,
115+ }
116+
107117/// A unified canvas based on wgpu that works on both native and web platforms.
108118#[ allow( dead_code) ]
109119pub struct WgpuCanvas {
@@ -126,6 +136,11 @@ pub struct WgpuCanvas {
126136 sample_count : u32 ,
127137 /// Texture for reading back pixels (for screenshots)
128138 readback_texture : wgpu:: Texture ,
139+ /// Staging buffer reused across `read_pixels` calls, grown on demand, so
140+ /// per-frame capture doesn't allocate (and free) a GPU buffer every call.
141+ screenshot_staging : RefCell < Option < wgpu:: Buffer > > ,
142+ /// Readback started by `begin_read_pixels`, completed by `finish_read_pixels`.
143+ snap_pending : RefCell < Option < PendingSnap > > ,
129144 /// Pending events from web callbacks (WASM only)
130145 #[ cfg( target_arch = "wasm32" ) ]
131146 pending_events : Rc < RefCell < Vec < WindowEvent > > > ,
@@ -665,6 +680,8 @@ impl WgpuCanvas {
665680 msaa_view,
666681 sample_count,
667682 readback_texture,
683+ screenshot_staging : RefCell :: new ( None ) ,
684+ snap_pending : RefCell :: new ( None ) ,
668685 #[ cfg( target_arch = "wasm32" ) ]
669686 pending_events,
670687 #[ cfg( target_arch = "wasm32" ) ]
@@ -775,6 +792,8 @@ impl WgpuCanvas {
775792 msaa_view,
776793 sample_count,
777794 readback_texture,
795+ screenshot_staging : RefCell :: new ( None ) ,
796+ snap_pending : RefCell :: new ( None ) ,
778797 #[ cfg( target_arch = "wasm32" ) ]
779798 pending_events : Rc :: new ( RefCell :: new ( Vec :: new ( ) ) ) ,
780799 #[ cfg( target_arch = "wasm32" ) ]
@@ -1310,6 +1329,25 @@ impl WgpuCanvas {
13101329 /// Reads pixels from the readback texture into the provided buffer.
13111330 /// Returns RGB data (3 bytes per pixel).
13121331 pub fn read_pixels ( & self , out : & mut Vec < u8 > , x : usize , y : usize , width : usize , height : usize ) {
1332+ self . begin_read_pixels ( x, y, width, height) ;
1333+ self . finish_read_pixels ( out) ;
1334+ }
1335+
1336+ /// Starts an asynchronous GPU→CPU readback of the readback texture and
1337+ /// returns immediately: it enqueues the texture→buffer copy and the buffer
1338+ /// map, but never waits on the GPU. Complete it — typically one frame
1339+ /// later, once the GPU has long finished the copy — with
1340+ /// [`Self::finish_read_pixels`]. Pipelining capture this way hides the
1341+ /// full CPU↔GPU sync that a blocking [`Self::read_pixels`] pays every
1342+ /// frame.
1343+ ///
1344+ /// A second `begin` before the previous readback was finished completes
1345+ /// and discards the previous one first (one readback in flight at a time).
1346+ pub fn begin_read_pixels ( & self , x : usize , y : usize , width : usize , height : usize ) {
1347+ if self . snap_pending . borrow ( ) . is_some ( ) {
1348+ self . finish_read_pixels ( & mut Vec :: new ( ) ) ;
1349+ }
1350+
13131351 let ctxt = Context :: get ( ) ;
13141352
13151353 // Calculate buffer size with alignment
@@ -1320,13 +1358,19 @@ impl WgpuCanvas {
13201358 let padded_bytes_per_row = unpadded_bytes_per_row. div_ceil ( align) * align;
13211359 let buffer_size = padded_bytes_per_row * height;
13221360
1323- // Create staging buffer
1324- let staging_buffer = ctxt. create_buffer ( & wgpu:: BufferDescriptor {
1325- label : Some ( "screenshot_staging_buffer" ) ,
1326- size : buffer_size as u64 ,
1327- usage : wgpu:: BufferUsages :: COPY_DST | wgpu:: BufferUsages :: MAP_READ ,
1328- mapped_at_creation : false ,
1329- } ) ;
1361+ // Reuse the cached staging buffer when it is large enough; (re)create
1362+ // it otherwise. Capturing every frame (video export) then allocates
1363+ // exactly once instead of once per call.
1364+ let mut staging_slot = self . screenshot_staging . borrow_mut ( ) ;
1365+ let staging_buffer = match staging_slot. take ( ) {
1366+ Some ( buffer) if buffer. size ( ) >= buffer_size as u64 => buffer,
1367+ _ => ctxt. create_buffer ( & wgpu:: BufferDescriptor {
1368+ label : Some ( "screenshot_staging_buffer" ) ,
1369+ size : buffer_size as u64 ,
1370+ usage : wgpu:: BufferUsages :: COPY_DST | wgpu:: BufferUsages :: MAP_READ ,
1371+ mapped_at_creation : false ,
1372+ } ) ,
1373+ } ;
13301374
13311375 // Copy from readback texture to staging buffer
13321376 let mut encoder = ctxt. create_command_encoder ( Some ( "screenshot_copy_encoder" ) ) ;
@@ -1357,20 +1401,55 @@ impl WgpuCanvas {
13571401 } ,
13581402 ) ;
13591403
1360- ctxt. submit ( std:: iter:: once ( encoder. finish ( ) ) ) ;
1404+ let submission = ctxt. submit_indexed ( std:: iter:: once ( encoder. finish ( ) ) ) ;
13611405
1362- // Map the buffer and read the data
1406+ // Queue the map; completion is observed in `finish_read_pixels`.
13631407 let buffer_slice = staging_buffer. slice ( ..) ;
13641408 let ( tx, rx) = std:: sync:: mpsc:: channel ( ) ;
13651409 buffer_slice. map_async ( wgpu:: MapMode :: Read , move |result| {
13661410 tx. send ( result) . unwrap ( ) ;
13671411 } ) ;
13681412
1369- // Wait for the GPU to finish
1370- let _ = ctxt. device . poll ( wgpu:: PollType :: wait_indefinitely ( ) ) ;
1413+ * self . snap_pending . borrow_mut ( ) = Some ( PendingSnap {
1414+ buffer : staging_buffer,
1415+ rx,
1416+ submission,
1417+ width,
1418+ height,
1419+ padded_bytes_per_row,
1420+ } ) ;
1421+ }
1422+
1423+ /// Completes a readback started by [`Self::begin_read_pixels`], filling
1424+ /// `out` with RGB data (3 bytes per pixel, rows bottom-to-top like
1425+ /// [`Self::read_pixels`]) and returning the captured `(width, height)`.
1426+ /// Returns `None` (and leaves `out` untouched) when no readback is in
1427+ /// flight. Blocks only until the copy's submission completes — a no-op
1428+ /// when a frame of GPU work has been submitted since the `begin`.
1429+ pub fn finish_read_pixels ( & self , out : & mut Vec < u8 > ) -> Option < ( u32 , u32 ) > {
1430+ let PendingSnap {
1431+ buffer : staging_buffer,
1432+ rx,
1433+ submission,
1434+ width,
1435+ height,
1436+ padded_bytes_per_row,
1437+ } = self . snap_pending . borrow_mut ( ) . take ( ) ?;
1438+ let ctxt = Context :: get ( ) ;
1439+
1440+ // Wait only for the copy submission (and, transitively, the work it
1441+ // depends on) instead of polling the device indefinitely.
1442+ let _ = ctxt. device . poll ( wgpu:: PollType :: Wait {
1443+ submission_index : Some ( submission) ,
1444+ timeout : None ,
1445+ } ) ;
13711446 rx. recv ( ) . unwrap ( ) . unwrap ( ) ;
13721447
1448+ let bytes_per_pixel = 4 ; // RGBA or BGRA
1449+ let unpadded_bytes_per_row = width * bytes_per_pixel;
1450+
13731451 // Read the data
1452+ let buffer_slice = staging_buffer. slice ( ..) ;
13741453 let data = buffer_slice. get_mapped_range ( ) ;
13751454
13761455 // Convert from BGRA/RGBA to RGB and handle row padding
@@ -1384,27 +1463,31 @@ impl WgpuCanvas {
13841463 ) ;
13851464
13861465 // wgpu has origin at top-left, but we want bottom-left origin for OpenGL compatibility
1387- // So we read rows in reverse order
1466+ // So we read rows in reverse order.
1467+ //
1468+ // The mapped range is uncached (write-combined) memory: scalar reads
1469+ // from it run at ~10 MB/s. memcpy each row into a cached local buffer
1470+ // first, then convert — orders of magnitude faster than indexing the
1471+ // mapped slice per byte.
1472+ let mut row_buf = vec ! [ 0u8 ; unpadded_bytes_per_row] ;
13881473 for row in ( 0 ..height) . rev ( ) {
13891474 let row_start = row * padded_bytes_per_row;
1390- for col in 0 ..width {
1391- let pixel_start = row_start + col * bytes_per_pixel;
1475+ row_buf . copy_from_slice ( & data [ row_start..row_start + unpadded_bytes_per_row ] ) ;
1476+ for px in row_buf . chunks_exact ( bytes_per_pixel) {
13921477 if is_bgra {
1393- // BGRA -> RGB
1394- out. push ( data[ pixel_start + 2 ] ) ; // R
1395- out. push ( data[ pixel_start + 1 ] ) ; // G
1396- out. push ( data[ pixel_start] ) ; // B
1478+ out. extend_from_slice ( & [ px[ 2 ] , px[ 1 ] , px[ 0 ] ] ) ;
13971479 } else {
1398- // RGBA -> RGB
1399- out. push ( data[ pixel_start] ) ; // R
1400- out. push ( data[ pixel_start + 1 ] ) ; // G
1401- out. push ( data[ pixel_start + 2 ] ) ; // B
1480+ out. extend_from_slice ( & [ px[ 0 ] , px[ 1 ] , px[ 2 ] ] ) ;
14021481 }
14031482 }
14041483 }
14051484
14061485 drop ( data) ;
14071486 staging_buffer. unmap ( ) ;
1487+ // Return the buffer to the cache so the next `begin_read_pixels`
1488+ // (or blocking `read_pixels`) reuses it instead of allocating.
1489+ * self . screenshot_staging . borrow_mut ( ) = Some ( staging_buffer) ;
1490+ Some ( ( width as u32 , height as u32 ) )
14081491 }
14091492
14101493 /// Gets the depth texture view for rendering.
0 commit comments