#![cfg(feature = "hardware-render")] use dpi::PhysicalSize; use ely_servo_host::{HardwareOffscreenContext, IOSurfaceHandle, IOSurfaceIdentity}; use servo::RenderingContext; #[cfg(target_os = "macos")] const HARDWARE_HOST_CHILD_ENV: &str = "ELY_SERVO_HARDWARE_HOST_CHILD"; #[test] fn identity_uses_handle_dimensions() { let handle = IOSurfaceHandle { mach_port_name: 7, surface_id: 11, width: 640, height: 480 }; assert_eq!( IOSurfaceIdentity::from_handle(handle), IOSurfaceIdentity { surface_id: 11, width: 640, height: 480 } ); } #[cfg(not(target_os = "macos"))] #[test] fn hardware_context_constructs() -> Result<(), String> { let context = HardwareOffscreenContext::new(PhysicalSize::new(64, 64)) .map_err(|error| format!("hardware context creation failed: {error:?}"))?; assert_eq!(context.size(), PhysicalSize::new(64, 64)); Ok(()) } #[cfg(target_os = "macos")] #[test] fn presented_surface_exposes_iosurface_identity_and_mach_port() -> Result<(), String> { let size = PhysicalSize::new(256, 192); let context = HardwareOffscreenContext::new(size) .map_err(|error| format!("hardware context creation failed: {error:?}"))?; assert_eq!( context .peek_iosurface_identity() .map_err(|error| format!("identity probe failed: {error:?}"))?, None ); context.make_current().map_err(|error| format!("make current failed: {error:?}"))?; context.prepare_for_rendering(); context.present(); let identity = context .peek_iosurface_identity() .map_err(|error| format!("identity probe failed: {error:?}"))? .ok_or_else(|| "present did not expose an IOSurface".to_string())?; let handle = context .current_iosurface_mach_port() .map_err(|error| format!("Mach port creation failed: {error:?}"))?; assert_ne!(handle.mach_port_name, 0); assert_eq!(identity, IOSurfaceIdentity::from_handle(handle)); assert_eq!(identity.width, size.width); assert_eq!(identity.height, size.height); assert_eq!(deallocate_mach_port(handle.mach_port_name), mach2::kern_return::KERN_SUCCESS); Ok(()) } #[cfg(target_os = "macos")] #[test] fn hardware_host_paints_and_presents_an_iosurface() -> Result<(), Box> { use std::env; use std::process::{Command, Stdio}; if env::var_os(HARDWARE_HOST_CHILD_ENV).is_some() { return exercise_hardware_host(); } let output = Command::new(env::current_exe()?) .arg("--exact") .arg("hardware_host_paints_and_presents_an_iosurface") .env(HARDWARE_HOST_CHILD_ENV, "1") .stdout(Stdio::piped()) .stderr(Stdio::piped()) .output()?; if output.status.success() { return Ok(()); } Err(format!( "hardware host child failed\nstatus: {}\nstdout: {}\nstderr: {}", output.status, String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ) .into()) } #[cfg(target_os = "macos")] fn exercise_hardware_host() -> Result<(), Box> { use std::thread; use std::time::Duration; use ely_domain::{ProfileId, TabId, UrlText}; use ely_servo_host::{ NavigationRequest, RenderingContextKind, ServoHost, ServoSurfaceSize, SoftwareServoHost, }; let size = ServoSurfaceSize::new(320, 240); let mut host = SoftwareServoHost::new_with_config_dir_and_kind( size, None, RenderingContextKind::Hardware, )?; let tab_id = TabId::new(); let webview_id = host.create_webview(tab_id.clone(), ProfileId::new())?; host.navigate(NavigationRequest { webview_id: webview_id.clone(), tab_id, url: UrlText::parse("data:text/html,%3Cbody%20style%3D%27background%3A%230369a1%27%3E")?, })?; for _ in 0..5_000 { host.tick(); if host.snapshot(&webview_id)?.has_pending_frame() { host.paint_without_readback(&webview_id)?; if host.peek_iosurface_identity(&webview_id)?.is_some() { let handle = host .current_iosurface_handle(&webview_id)? .ok_or("hardware webview did not expose an IOSurface handle")?; assert_eq!(handle.width, 320); assert_eq!(handle.height, 240); assert_eq!( deallocate_mach_port(handle.mach_port_name), mach2::kern_return::KERN_SUCCESS ); return Ok(()); } } thread::sleep(Duration::from_millis(2)); } Err("timed out waiting for a hardware IOSurface frame".into()) } #[cfg(target_os = "macos")] #[expect(unsafe_code)] fn deallocate_mach_port(name: u32) -> i32 { // `name` is a live send right minted by IOSurfaceCreateMachPort in this task. unsafe { mach2::mach_port::mach_port_deallocate(mach2::traps::mach_task_self(), name) } }