From b989d04cb423cd0bdc8fa4956d14c132979d4790 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 19 Aug 2026 22:28:57 +0000 Subject: [PATCH] Fix Android gRPC raw-frame vertical inversion --- .../src/emulator/raw.rs | 87 +++++++++++-------- 1 file changed, 53 insertions(+), 34 deletions(-) diff --git a/packages/accessibility-android-sys/src/emulator/raw.rs b/packages/accessibility-android-sys/src/emulator/raw.rs index 20e5be5..3b64c5a 100644 --- a/packages/accessibility-android-sys/src/emulator/raw.rs +++ b/packages/accessibility-android-sys/src/emulator/raw.rs @@ -114,24 +114,7 @@ impl RawFrameStream { if width == 0 || height == 0 { continue; } - let expected = rgba_len(width, height)?; - let mut pixels = if let Some(mapping) = &self.mapping { - let source = mapping - .get(..expected) - .ok_or_else(|| anyhow!("emulator frame exceeds its memory mapping"))?; - copy_bottom_up_rgba(source, width, height) - } else { - if image.image.len() != expected { - bail!( - "RGBA frame is {} bytes, expected {expected} for {width}x{height}", - image.image.len() - ); - } - image.image - }; - if self.mapping.is_none() { - flip_vertical_rgba(&mut pixels, width, height); - } + let pixels = top_down_rgba(self.mapping.as_deref(), image.image, width, height)?; return Ok(Some(RawFrame { pixels, width, @@ -162,6 +145,31 @@ fn rgba_len(width: u32, height: u32) -> Result { .context("Android frame dimensions overflow") } +/// Normalizes a frame to top-down RGBA. MMAP frames arrive bottom-up and are +/// flipped; gRPC frames already arrive top-down and pass through unchanged. +fn top_down_rgba( + mapping: Option<&[u8]>, + image: Vec, + width: u32, + height: u32, +) -> Result> { + let expected = rgba_len(width, height)?; + if let Some(mapping) = mapping { + let source = mapping + .get(..expected) + .ok_or_else(|| anyhow!("emulator frame exceeds its memory mapping"))?; + Ok(copy_bottom_up_rgba(source, width, height)) + } else { + if image.len() != expected { + bail!( + "RGBA frame is {} bytes, expected {expected} for {width}x{height}", + image.len() + ); + } + Ok(image) + } +} + fn copy_bottom_up_rgba(source: &[u8], width: u32, height: u32) -> Vec { let stride = width as usize * 4; let mut pixels = vec![0; source.len()]; @@ -174,15 +182,6 @@ fn copy_bottom_up_rgba(source: &[u8], width: u32, height: u32) -> Vec { pixels } -fn flip_vertical_rgba(pixels: &mut [u8], width: u32, height: u32) { - let stride = width as usize * 4; - for row in 0..height as usize / 2 { - let opposite = height as usize - 1 - row; - let (before, after) = pixels.split_at_mut(opposite * stride); - before[row * stride..(row + 1) * stride].swap_with_slice(&mut after[..stride]); - } -} - fn mapping_path() -> PathBuf { let nonce = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -209,13 +208,6 @@ fn file_uri(path: &std::path::Path) -> String { mod tests { use super::*; - #[test] - fn flips_bottom_up_rgba() { - let mut pixels = vec![1, 2, 3, 4, 5, 6, 7, 8]; - flip_vertical_rgba(&mut pixels, 1, 2); - assert_eq!(pixels, vec![5, 6, 7, 8, 1, 2, 3, 4]); - } - #[test] fn copies_bottom_up_rgba() { assert_eq!( @@ -223,4 +215,31 @@ mod tests { vec![5, 6, 7, 8, 1, 2, 3, 4] ); } + + /// A 2x3 frame with a distinct value per row; any vertical inversion + /// reorders the rows and fails the comparison. + fn rows_2x3(top_to_bottom: [u8; 3]) -> Vec { + top_to_bottom + .iter() + .flat_map(|&row| std::iter::repeat_n(row, 8)) + .collect() + } + + #[test] + fn grpc_frames_pass_through_top_down() { + let top_down = rows_2x3([1, 2, 3]); + assert_eq!( + top_down_rgba(None, top_down.clone(), 2, 3).unwrap(), + top_down + ); + } + + #[test] + fn mmap_frames_are_flipped_to_top_down() { + let bottom_up = rows_2x3([3, 2, 1]); + assert_eq!( + top_down_rgba(Some(&bottom_up), Vec::new(), 2, 3).unwrap(), + rows_2x3([1, 2, 3]) + ); + } }