Fix Servo IOSurface orientation and resize identity
This commit is contained in:
@@ -37,7 +37,13 @@ use thiserror::Error;
|
||||
/// Constructed lazily by the renderer-side client on the first
|
||||
/// hardware-path frame.
|
||||
pub(crate) struct IOSurfaceCache {
|
||||
pixel_buffers: HashMap<u64, CVPixelBuffer>,
|
||||
pixel_buffers: HashMap<u64, CachedPixelBuffer>,
|
||||
}
|
||||
|
||||
struct CachedPixelBuffer {
|
||||
pixel_buffer: CVPixelBuffer,
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -54,21 +60,15 @@ impl IOSurfaceCache {
|
||||
}
|
||||
|
||||
/// Import an IOSurface published by the sidecar's
|
||||
/// `surface_handle` field. Idempotent on `surface_id`: a second
|
||||
/// call with the same id immediately deallocates the duplicate
|
||||
/// mach port without re-importing. The sender's T10.3 dedup means
|
||||
/// the duplicate path should never fire in practice — it's here
|
||||
/// so a misbehaving sidecar can't quietly leak ports.
|
||||
/// `surface_handle` field. Idempotent on `surface_id` plus pixel
|
||||
/// dimensions: duplicate handles for the same sized IOSurface are
|
||||
/// discarded, while a resized IOSurface that reuses the same
|
||||
/// `surface_id` replaces the cached pixel buffer.
|
||||
pub fn import(
|
||||
&mut self,
|
||||
mach_port_name: u32,
|
||||
surface_id: u64,
|
||||
) -> Result<(), SurfaceImportError> {
|
||||
if self.pixel_buffers.contains_key(&surface_id) {
|
||||
deallocate_mach_port(mach_port_name);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(iosurface) = objc2_io_surface::IOSurfaceRef::lookup_from_mach_port(mach_port_name)
|
||||
else {
|
||||
return Err(SurfaceImportError::LookupFailed { port: mach_port_name });
|
||||
@@ -91,8 +91,19 @@ impl IOSurfaceCache {
|
||||
|
||||
let pixel_buffer = CVPixelBuffer::from_io_surface(&io_surface_view, None)
|
||||
.map_err(|status| SurfaceImportError::PixelBufferBuildFailed { status })?;
|
||||
let width = pixel_buffer.get_width() as u32;
|
||||
let height = pixel_buffer.get_height() as u32;
|
||||
|
||||
self.pixel_buffers.insert(surface_id, pixel_buffer);
|
||||
if self
|
||||
.pixel_buffers
|
||||
.get(&surface_id)
|
||||
.is_some_and(|cached| cached.width == width && cached.height == height)
|
||||
{
|
||||
deallocate_mach_port(mach_port_name);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.pixel_buffers.insert(surface_id, CachedPixelBuffer { pixel_buffer, width, height });
|
||||
deallocate_mach_port(mach_port_name);
|
||||
Ok(())
|
||||
}
|
||||
@@ -104,7 +115,7 @@ impl IOSurfaceCache {
|
||||
/// atomic increment) so the caller can hand it to GPUI's
|
||||
/// `surface(...)` element without holding a borrow on the cache.
|
||||
pub fn pixel_buffer_for(&self, surface_id: u64) -> Option<CVPixelBuffer> {
|
||||
self.pixel_buffers.get(&surface_id).cloned()
|
||||
self.pixel_buffers.get(&surface_id).map(|cached| cached.pixel_buffer.clone())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -156,9 +167,6 @@ mod tests {
|
||||
};
|
||||
use std::os::raw::c_void;
|
||||
|
||||
const TEST_WIDTH: u32 = 64;
|
||||
const TEST_HEIGHT: u32 = 48;
|
||||
|
||||
/// Build a CPU-backed IOSurface from scratch, the same way
|
||||
/// surfman's macOS backend does.
|
||||
///
|
||||
@@ -167,13 +175,13 @@ mod tests {
|
||||
///
|
||||
/// The pointer-casts mirror
|
||||
/// `surfman::platform::macos::system::surface::create_io_surface`.
|
||||
fn build_local_iosurface() -> Result<CFRetained<IOSurfaceRef>, String> {
|
||||
fn build_local_iosurface(width: u32, height: u32) -> Result<CFRetained<IOSurfaceRef>, String> {
|
||||
let pixel_format: i32 = i32::from_be_bytes(*b"BGRA");
|
||||
let bytes_per_element: i32 = 4;
|
||||
let bytes_per_row: i32 = (TEST_WIDTH as i32) * bytes_per_element;
|
||||
let bytes_per_row: i32 = (width as i32) * bytes_per_element;
|
||||
|
||||
let width_num = CFNumber::new_i32(TEST_WIDTH as i32);
|
||||
let height_num = CFNumber::new_i32(TEST_HEIGHT as i32);
|
||||
let width_num = CFNumber::new_i32(width as i32);
|
||||
let height_num = CFNumber::new_i32(height as i32);
|
||||
let bpe_num = CFNumber::new_i32(bytes_per_element);
|
||||
let bpr_num = CFNumber::new_i32(bytes_per_row);
|
||||
let pf_num = CFNumber::new_i32(pixel_format);
|
||||
@@ -207,7 +215,7 @@ mod tests {
|
||||
#[test]
|
||||
fn imports_local_iosurface_into_pixel_buffer() -> Result<(), String> {
|
||||
let mut cache = IOSurfaceCache::new();
|
||||
let iosurface = build_local_iosurface()?;
|
||||
let iosurface = build_local_iosurface(64, 48)?;
|
||||
let mach_port = iosurface.create_mach_port();
|
||||
assert!(mach_port != 0, "IOSurfaceCreateMachPort must yield a real port");
|
||||
let surface_id: u64 = 0xDEAD_BEEFu64;
|
||||
@@ -219,12 +227,12 @@ mod tests {
|
||||
.ok_or_else(|| "imported pixel buffer was missing".to_string())?;
|
||||
assert_eq!(
|
||||
pixel_buffer.get_width() as u32,
|
||||
TEST_WIDTH,
|
||||
64,
|
||||
"CVPixelBuffer width must match the source IOSurface",
|
||||
);
|
||||
assert_eq!(
|
||||
pixel_buffer.get_height() as u32,
|
||||
TEST_HEIGHT,
|
||||
48,
|
||||
"CVPixelBuffer height must match the source IOSurface",
|
||||
);
|
||||
assert_eq!(cache.cached_surface_count(), 1);
|
||||
@@ -234,7 +242,7 @@ mod tests {
|
||||
#[test]
|
||||
fn second_import_with_same_surface_id_is_idempotent() -> Result<(), String> {
|
||||
let mut cache = IOSurfaceCache::new();
|
||||
let iosurface = build_local_iosurface()?;
|
||||
let iosurface = build_local_iosurface(64, 48)?;
|
||||
let port_a = iosurface.create_mach_port();
|
||||
let port_b = iosurface.create_mach_port();
|
||||
assert!(port_a != 0 && port_b != 0 && port_a != port_b);
|
||||
@@ -246,4 +254,23 @@ mod tests {
|
||||
assert_eq!(cache.cached_surface_count(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_surface_id_with_changed_dimensions_replaces_pixel_buffer() -> Result<(), String> {
|
||||
let mut cache = IOSurfaceCache::new();
|
||||
let initial = build_local_iosurface(64, 48)?;
|
||||
let resized = build_local_iosurface(96, 72)?;
|
||||
let surface_id = 0xBBBB_BBBB;
|
||||
|
||||
cache.import(initial.create_mach_port(), surface_id).map_err(|error| error.to_string())?;
|
||||
cache.import(resized.create_mach_port(), surface_id).map_err(|error| error.to_string())?;
|
||||
|
||||
let pixel_buffer = cache
|
||||
.pixel_buffer_for(surface_id)
|
||||
.ok_or_else(|| "resized pixel buffer was missing".to_string())?;
|
||||
assert_eq!(pixel_buffer.get_width() as u32, 96);
|
||||
assert_eq!(pixel_buffer.get_height() as u32, 72);
|
||||
assert_eq!(cache.cached_surface_count(), 1);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ use super::WebSurfaceStore;
|
||||
|
||||
const LIVE_SURFACE_WIDTH: u32 = 934;
|
||||
const LIVE_SURFACE_HEIGHT: u32 = 657;
|
||||
const RESIZED_LIVE_SURFACE_WIDTH: u32 = LIVE_SURFACE_WIDTH + 12;
|
||||
const RESIZED_LIVE_SURFACE_HEIGHT: u32 = LIVE_SURFACE_HEIGHT + 20;
|
||||
const MINIMUM_CONTENT_PIXELS: u64 = 1_000;
|
||||
const LIVE_SITE_RENDER_ATTEMPTS: usize = 3;
|
||||
const LIVE_SITE_WAIT_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
@@ -64,6 +66,13 @@ fn web_surface_scrolls_prd_site_down_and_up() -> Result<(), Box<dyn Error>> {
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_surface_resizes_prd_site_without_failed_state() -> Result<(), Box<dyn Error>> {
|
||||
run_isolated_live_site_test("web_surface_resizes_prd_site_without_failed_state", || {
|
||||
assert_web_surface_resizes_prd_site()
|
||||
})
|
||||
}
|
||||
|
||||
fn run_isolated_live_site_test(
|
||||
test_name: &str,
|
||||
test: impl FnOnce() -> Result<(), Box<dyn Error>>,
|
||||
@@ -166,6 +175,54 @@ fn assert_web_surface_scrolls_prd_site() -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn assert_web_surface_resizes_prd_site() -> Result<(), Box<dyn Error>> {
|
||||
let mut store = WebSurfaceStore::new();
|
||||
let profile_id = ProfileId::new();
|
||||
let case = PRD_TOP_SITE_CASES
|
||||
.iter()
|
||||
.find(|case| case.url == "https://servo.org/")
|
||||
.ok_or("missing servo.org live-site case")?;
|
||||
let tab = web_tab(profile_id, case.url)?;
|
||||
|
||||
assert_eq!(
|
||||
store.record_viewport_size(tab.id(), live_surface_bounds(), 1.0),
|
||||
WebSurfaceInputOutcome::Applied,
|
||||
"{}",
|
||||
case.url,
|
||||
);
|
||||
store.ensure_surface(&tab, ProfileDataMode::Transient, &[]);
|
||||
let _ = wait_for_ready_frame_at_size(
|
||||
&mut store,
|
||||
tab.id(),
|
||||
case,
|
||||
LIVE_SURFACE_WIDTH,
|
||||
LIVE_SURFACE_HEIGHT,
|
||||
)?;
|
||||
|
||||
assert_eq!(
|
||||
store.record_viewport_size(tab.id(), resized_live_surface_bounds(), 1.0),
|
||||
WebSurfaceInputOutcome::Buffered,
|
||||
"{}",
|
||||
case.url,
|
||||
);
|
||||
assert_eq!(
|
||||
store.record_viewport_size(tab.id(), resized_live_surface_bounds(), 1.0),
|
||||
WebSurfaceInputOutcome::Applied,
|
||||
"{}",
|
||||
case.url,
|
||||
);
|
||||
store.ensure_surface(&tab, ProfileDataMode::Transient, &[]);
|
||||
wait_for_ready_frame_at_size(
|
||||
&mut store,
|
||||
tab.id(),
|
||||
case,
|
||||
RESIZED_LIVE_SURFACE_WIDTH,
|
||||
RESIZED_LIVE_SURFACE_HEIGHT,
|
||||
)?;
|
||||
store.close_surface(tab.id());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_web_surface_frame(
|
||||
store: &mut WebSurfaceStore,
|
||||
profile_id: &ProfileId,
|
||||
@@ -283,6 +340,49 @@ fn wait_for_ready_frame_at_scroll(
|
||||
}
|
||||
}
|
||||
|
||||
fn wait_for_ready_frame_at_size(
|
||||
store: &mut WebSurfaceStore,
|
||||
tab_id: &TabId,
|
||||
case: &LiveSiteCase,
|
||||
expected_width: u32,
|
||||
expected_height: u32,
|
||||
) -> Result<WebSurfaceFrame, String> {
|
||||
let started_at = Instant::now();
|
||||
let mut last_error = None;
|
||||
|
||||
loop {
|
||||
if started_at.elapsed() >= LIVE_SITE_WAIT_TIMEOUT {
|
||||
return Err(last_error.unwrap_or_else(|| {
|
||||
format!("timed out rendering {} at {expected_width}x{expected_height}", case.url)
|
||||
}));
|
||||
}
|
||||
|
||||
store.tick(std::slice::from_ref(tab_id));
|
||||
match store.state(tab_id) {
|
||||
Some(WebSurfaceState::Ready(frame))
|
||||
if frame.size().width == expected_width
|
||||
&& frame.size().height == expected_height =>
|
||||
{
|
||||
if let Err(error) =
|
||||
validate_prd_frame_at_size(frame, case, expected_width, expected_height)
|
||||
{
|
||||
last_error = Some(error);
|
||||
thread::sleep(LIVE_SITE_WAIT_INTERVAL);
|
||||
continue;
|
||||
}
|
||||
return Ok(frame.clone());
|
||||
}
|
||||
Some(WebSurfaceState::Ready(_)) => {}
|
||||
Some(WebSurfaceState::Failed { message, .. }) => {
|
||||
return Err(format!("{} failed: {message}", case.url));
|
||||
}
|
||||
Some(WebSurfaceState::Loading { .. }) | None => {}
|
||||
}
|
||||
|
||||
thread::sleep(LIVE_SITE_WAIT_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_prd_frame(
|
||||
frame: &WebSurfaceFrame,
|
||||
case: &LiveSiteCase,
|
||||
@@ -330,6 +430,41 @@ fn validate_prd_frame(
|
||||
require(frame.sample_hash() > 0, case.url.to_string())
|
||||
}
|
||||
|
||||
fn validate_prd_frame_at_size(
|
||||
frame: &WebSurfaceFrame,
|
||||
case: &LiveSiteCase,
|
||||
expected_width: u32,
|
||||
expected_height: u32,
|
||||
) -> Result<(), String> {
|
||||
require(
|
||||
frame.size()
|
||||
== WebSurfaceSize {
|
||||
width: expected_width,
|
||||
height: expected_height,
|
||||
device_pixel_ratio_percent: 100,
|
||||
},
|
||||
format!("{} size: {:?}", case.url, frame.size()),
|
||||
)?;
|
||||
require_render_state_is_open(frame.render_state(), case.url)?;
|
||||
require(
|
||||
frame.url_label().contains(normalized_url(case.url)),
|
||||
format!("url: {}", frame.url_label()),
|
||||
)?;
|
||||
require(
|
||||
frame.title_label().contains(case.title_fragment),
|
||||
format!("title: {}", frame.title_label()),
|
||||
)?;
|
||||
if frame.has_hardware_surface() {
|
||||
return Ok(());
|
||||
}
|
||||
require(frame.non_white_pixel_count() > 0, case.url.to_string())?;
|
||||
require(
|
||||
frame.content_pixel_count() >= MINIMUM_CONTENT_PIXELS,
|
||||
format!("{} content pixels: {}", case.url, frame.content_pixel_count()),
|
||||
)?;
|
||||
require(frame.sample_hash() > 0, case.url.to_string())
|
||||
}
|
||||
|
||||
fn log_prd_frame(label: &str, frame: &WebSurfaceFrame, case: &LiveSiteCase) {
|
||||
eprintln!(
|
||||
"prd-live-site {label} url={} loaded={} title={} state={} size={}x{} content_pixels={} non_white_pixels={} sample_hash={}",
|
||||
@@ -360,6 +495,13 @@ fn live_surface_bounds() -> Bounds<gpui::Pixels> {
|
||||
)
|
||||
}
|
||||
|
||||
fn resized_live_surface_bounds() -> Bounds<gpui::Pixels> {
|
||||
Bounds::new(
|
||||
point(px(0.0), px(0.0)),
|
||||
size(px(RESIZED_LIVE_SURFACE_WIDTH as f32), px(RESIZED_LIVE_SURFACE_HEIGHT as f32)),
|
||||
)
|
||||
}
|
||||
|
||||
fn live_scroll_point() -> gpui::Point<gpui::Pixels> {
|
||||
point(px(LIVE_SITE_SCROLL_POINT_X), px(LIVE_SITE_SCROLL_POINT_Y))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user