perf(app): gate payloadless surfaces on app imports
This commit is contained in:
@@ -73,33 +73,18 @@ impl IOSurfaceCache {
|
|||||||
/// dimensions: duplicate handles for the same sized IOSurface are
|
/// dimensions: duplicate handles for the same sized IOSurface are
|
||||||
/// discarded, while a resized IOSurface that reuses the same
|
/// discarded, while a resized IOSurface that reuses the same
|
||||||
/// `surface_id` replaces the cached pixel buffer.
|
/// `surface_id` replaces the cached pixel buffer.
|
||||||
|
#[cfg(test)]
|
||||||
pub fn import(
|
pub fn import(
|
||||||
&mut self,
|
&mut self,
|
||||||
mach_port_name: u32,
|
mach_port_name: u32,
|
||||||
surface_id: u64,
|
surface_id: u64,
|
||||||
) -> Result<(), SurfaceImportError> {
|
) -> Result<(), SurfaceImportError> {
|
||||||
let Some(iosurface) = objc2_io_surface::IOSurfaceRef::lookup_from_mach_port(mach_port_name)
|
let pixel_buffer = import_pixel_buffer_from_mach_port(mach_port_name)?;
|
||||||
else {
|
self.insert_pixel_buffer(surface_id, pixel_buffer);
|
||||||
return Err(SurfaceImportError::LookupFailed { port: mach_port_name });
|
Ok(())
|
||||||
};
|
}
|
||||||
|
|
||||||
// Both objc2-io-surface and the legacy `io_surface` crate wrap
|
pub(crate) fn insert_pixel_buffer(&mut self, surface_id: u64, pixel_buffer: CVPixelBuffer) {
|
||||||
// the same C `__IOSurface` pointer. CVPixelBufferCreateWithIOSurface
|
|
||||||
// (via core-video) expects the legacy crate's wrapper. Reach for
|
|
||||||
// the raw pointer and let TCFType CFRetain it independently so
|
|
||||||
// both Rust handles can drop without double-freeing.
|
|
||||||
let raw_ptr: *const std::ffi::c_void =
|
|
||||||
(&*iosurface) as *const objc2_io_surface::IOSurfaceRef as *const std::ffi::c_void;
|
|
||||||
#[allow(deprecated)]
|
|
||||||
let io_surface_view: IOSurface = {
|
|
||||||
#[expect(unsafe_code)]
|
|
||||||
unsafe {
|
|
||||||
IOSurface::wrap_under_get_rule(raw_ptr as io_surface::IOSurfaceRef)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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 width = pixel_buffer.get_width() as u32;
|
||||||
let height = pixel_buffer.get_height() as u32;
|
let height = pixel_buffer.get_height() as u32;
|
||||||
|
|
||||||
@@ -108,13 +93,10 @@ impl IOSurfaceCache {
|
|||||||
.get(&surface_id)
|
.get(&surface_id)
|
||||||
.is_some_and(|cached| cached.width == width && cached.height == height)
|
.is_some_and(|cached| cached.width == width && cached.height == height)
|
||||||
{
|
{
|
||||||
deallocate_mach_port(mach_port_name);
|
return;
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.pixel_buffers.insert(surface_id, CachedPixelBuffer { pixel_buffer, width, height });
|
self.pixel_buffers.insert(surface_id, CachedPixelBuffer { pixel_buffer, width, height });
|
||||||
deallocate_mach_port(mach_port_name);
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Look up an already-imported pixel buffer by `surface_id`. The
|
/// Look up an already-imported pixel buffer by `surface_id`. The
|
||||||
@@ -127,12 +109,51 @@ impl IOSurfaceCache {
|
|||||||
self.pixel_buffers.get(&surface_id).map(|cached| cached.pixel_buffer.clone())
|
self.pixel_buffers.get(&surface_id).map(|cached| cached.pixel_buffer.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn surface_ids(&self) -> Vec<u64> {
|
||||||
|
self.pixel_buffers.keys().copied().collect()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub fn cached_surface_count(&self) -> usize {
|
pub fn cached_surface_count(&self) -> usize {
|
||||||
self.pixel_buffers.len()
|
self.pixel_buffers.len()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn import_pixel_buffer_from_mach_port(
|
||||||
|
mach_port_name: u32,
|
||||||
|
) -> Result<CVPixelBuffer, SurfaceImportError> {
|
||||||
|
let result = build_pixel_buffer_from_mach_port(mach_port_name);
|
||||||
|
deallocate_mach_port(mach_port_name);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_pixel_buffer_from_mach_port(
|
||||||
|
mach_port_name: u32,
|
||||||
|
) -> Result<CVPixelBuffer, SurfaceImportError> {
|
||||||
|
let Some(iosurface) = objc2_io_surface::IOSurfaceRef::lookup_from_mach_port(mach_port_name)
|
||||||
|
else {
|
||||||
|
return Err(SurfaceImportError::LookupFailed { port: mach_port_name });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Both objc2-io-surface and the legacy `io_surface` crate wrap
|
||||||
|
// the same C `__IOSurface` pointer. CVPixelBufferCreateWithIOSurface
|
||||||
|
// (via core-video) expects the legacy crate's wrapper. Reach for
|
||||||
|
// the raw pointer and let TCFType CFRetain it independently so
|
||||||
|
// both Rust handles can drop without double-freeing.
|
||||||
|
let raw_ptr: *const std::ffi::c_void =
|
||||||
|
(&*iosurface) as *const objc2_io_surface::IOSurfaceRef as *const std::ffi::c_void;
|
||||||
|
#[allow(deprecated)]
|
||||||
|
let io_surface_view: IOSurface = {
|
||||||
|
#[expect(unsafe_code)]
|
||||||
|
unsafe {
|
||||||
|
IOSurface::wrap_under_get_rule(raw_ptr as io_surface::IOSurfaceRef)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
CVPixelBuffer::from_io_surface(&io_surface_view, None)
|
||||||
|
.map_err(|status| SurfaceImportError::PixelBufferBuildFailed { status })
|
||||||
|
}
|
||||||
|
|
||||||
/// Release one send right against the mach port we received. The
|
/// Release one send right against the mach port we received. The
|
||||||
/// IOSurface itself stays alive because the `CVPixelBuffer` (or the
|
/// IOSurface itself stays alive because the `CVPixelBuffer` (or the
|
||||||
/// sidecar's surfman) still retain it.
|
/// sidecar's surfman) still retain it.
|
||||||
|
|||||||
@@ -1,36 +1,37 @@
|
|||||||
use std::{
|
use std::{
|
||||||
io::{self, BufRead, BufReader, Read, Write},
|
io::{BufRead, BufReader, Read, Write},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
process::{Child, ChildStdin, ChildStdout, Stdio},
|
process::{Child, ChildStdin, ChildStdout, Stdio},
|
||||||
time::Duration,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[path = "servo_live_iosurface_importer.rs"]
|
||||||
|
mod iosurface_importer;
|
||||||
|
#[path = "servo_live_types.rs"]
|
||||||
|
mod types;
|
||||||
/// Environment variable that lets the user pick the rendering context
|
/// Environment variable that lets the user pick the rendering context
|
||||||
/// kind used by the spawned sidecar. Accepted values: `software`
|
/// kind used by the spawned sidecar. Accepted values: `software`
|
||||||
/// and `hardware`. macOS defaults to the hardware path and receives
|
/// and `hardware`. macOS defaults to the hardware path and receives
|
||||||
/// IOSurface mach send rights over a side Mach channel.
|
/// IOSurface mach send rights over a side Mach channel.
|
||||||
use ely_domain::SitePermissionDecision;
|
|
||||||
use serde::Serialize;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[path = "servo_live_wire.rs"]
|
#[path = "servo_live_wire.rs"]
|
||||||
mod wire;
|
mod wire;
|
||||||
|
|
||||||
|
pub(crate) use types::{
|
||||||
|
ServoLiveEnsureRequest, ServoLiveError, ServoLiveFrame, ServoLiveSitePermission,
|
||||||
|
};
|
||||||
|
|
||||||
use super::servo_sidecar_command::{
|
use super::servo_sidecar_command::{
|
||||||
SidecarCommandError, SidecarRenderingContext, default_sidecar_command,
|
SidecarRenderingContext, default_sidecar_command, rendering_context_from_env,
|
||||||
rendering_context_from_env,
|
|
||||||
};
|
};
|
||||||
use wire::{
|
use wire::{
|
||||||
LiveFrameReport, LiveRequest, LiveResponse, LiveSurfaceHandle, log_frame_perf,
|
LiveRequest, LiveResponse, LiveSurfaceHandle, log_frame_perf, log_iosurface_current,
|
||||||
log_iosurface_current, log_iosurface_handle,
|
log_iosurface_handle,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
use super::iosurface_mach::{IOSurfaceMachError, IOSurfaceMachReceiver};
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use super::iosurface_metal::IOSurfaceCache;
|
use super::iosurface_metal::IOSurfaceCache;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
use core_video::pixel_buffer::CVPixelBuffer;
|
use iosurface_importer::{IOSurfaceImportResult, IOSurfaceImportWorker};
|
||||||
|
|
||||||
pub(crate) struct ServoLiveClient {
|
pub(crate) struct ServoLiveClient {
|
||||||
child: Child,
|
child: Child,
|
||||||
@@ -42,7 +43,7 @@ pub(crate) struct ServoLiveClient {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
iosurface_cache: IOSurfaceCache,
|
iosurface_cache: IOSurfaceCache,
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
iosurface_receiver: Option<IOSurfaceMachReceiver>,
|
iosurface_importer: Option<IOSurfaceImportWorker>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServoLiveClient {
|
impl ServoLiveClient {
|
||||||
@@ -57,10 +58,13 @@ impl ServoLiveClient {
|
|||||||
command.arg("live").arg("--profile-data-dir").arg(profile_data_dir);
|
command.arg("live").arg("--profile-data-dir").arg(profile_data_dir);
|
||||||
command.arg("--rendering-context").arg(rendering_context.cli_arg());
|
command.arg("--rendering-context").arg(rendering_context.cli_arg());
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
let iosurface_receiver = if rendering_context == SidecarRenderingContext::Hardware {
|
let iosurface_importer = if rendering_context == SidecarRenderingContext::Hardware {
|
||||||
let receiver = IOSurfaceMachReceiver::new()?;
|
let receiver = super::iosurface_mach::IOSurfaceMachReceiver::new()?;
|
||||||
command.arg("--iosurface-mach-service").arg(receiver.service_name());
|
command.arg("--iosurface-mach-service").arg(receiver.service_name());
|
||||||
Some(receiver)
|
Some(
|
||||||
|
IOSurfaceImportWorker::new(receiver)
|
||||||
|
.map_err(ServoLiveError::IOSurfaceImportWorker)?,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -81,7 +85,7 @@ impl ServoLiveClient {
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
iosurface_cache: IOSurfaceCache::new(),
|
iosurface_cache: IOSurfaceCache::new(),
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
iosurface_receiver,
|
iosurface_importer,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +93,9 @@ impl ServoLiveClient {
|
|||||||
&mut self,
|
&mut self,
|
||||||
request: ServoLiveEnsureRequest,
|
request: ServoLiveEnsureRequest,
|
||||||
) -> Result<Option<ServoLiveFrame>, ServoLiveError> {
|
) -> Result<Option<ServoLiveFrame>, ServoLiveError> {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
self.drain_iosurface_imports()?;
|
||||||
|
let ready_surface_ids = self.ready_surface_ids();
|
||||||
self.request(LiveRequest::Ensure {
|
self.request(LiveRequest::Ensure {
|
||||||
tab_id: request.tab_id,
|
tab_id: request.tab_id,
|
||||||
profile_id: request.profile_id,
|
profile_id: request.profile_id,
|
||||||
@@ -107,11 +114,15 @@ impl ServoLiveClient {
|
|||||||
hover_y: request.hover_y,
|
hover_y: request.hover_y,
|
||||||
typed_text: request.typed_text,
|
typed_text: request.typed_text,
|
||||||
site_permissions: request.site_permissions,
|
site_permissions: request.site_permissions,
|
||||||
|
ready_surface_ids,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn poll(&mut self, tab_id: String) -> Result<Option<ServoLiveFrame>, ServoLiveError> {
|
pub fn poll(&mut self, tab_id: String) -> Result<Option<ServoLiveFrame>, ServoLiveError> {
|
||||||
self.request(LiveRequest::Poll { tab_id })
|
#[cfg(target_os = "macos")]
|
||||||
|
self.drain_iosurface_imports()?;
|
||||||
|
let ready_surface_ids = self.ready_surface_ids();
|
||||||
|
self.request(LiveRequest::Poll { tab_id, ready_surface_ids })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close(&mut self, tab_id: String) -> Result<(), ServoLiveError> {
|
pub fn close(&mut self, tab_id: String) -> Result<(), ServoLiveError> {
|
||||||
@@ -183,363 +194,93 @@ impl ServoLiveClient {
|
|||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
if let Some(handle) = surface_handle.as_ref() {
|
if let Some(handle) = surface_handle.as_ref() {
|
||||||
// Drain the stdout payload before IOSurface import so the
|
self.queue_iosurface_handle(*handle)?;
|
||||||
// sidecar cannot block writing RGBA bytes while this worker
|
self.drain_iosurface_imports()?;
|
||||||
// is inside IOSurfaceLookupFromMachPort.
|
|
||||||
self.import_iosurface_handle(handle)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
if let Some(surface_id) = current_surface_id {
|
if let Some(surface_id) = current_surface_id {
|
||||||
let pixel_buffer = self.iosurface_cache.pixel_buffer_for(surface_id);
|
let pixel_buffer = self.iosurface_cache.pixel_buffer_for(surface_id);
|
||||||
if pixel_buffer.is_none() && !has_software_payload {
|
if pixel_buffer.is_none() && !has_software_payload {
|
||||||
return Err(ServoLiveError::IOSurfacePixelBufferMissing { surface_id });
|
return Ok(None);
|
||||||
}
|
}
|
||||||
frame.pixel_buffer = pixel_buffer;
|
frame.set_pixel_buffer(pixel_buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Some(frame))
|
Ok(Some(frame))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
impl ServoLiveClient {
|
||||||
|
fn ready_surface_ids(&self) -> Vec<u64> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
impl ServoLiveClient {
|
||||||
|
fn ready_surface_ids(&self) -> Vec<u64> {
|
||||||
|
self.iosurface_cache.surface_ids()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn queue_iosurface_handle(&mut self, handle: LiveSurfaceHandle) -> Result<(), ServoLiveError> {
|
||||||
|
let Some(importer) = self.iosurface_importer.as_ref() else {
|
||||||
|
return Err(ServoLiveError::IOSurfaceImportFailed {
|
||||||
|
surface_id: handle.surface_id,
|
||||||
|
mach_port_name: handle.mach_port_name,
|
||||||
|
message: "IOSurface import worker is unavailable".to_string(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
importer.submit(handle).map_err(|failure| ServoLiveError::IOSurfaceImportFailed {
|
||||||
|
surface_id: failure.surface_id,
|
||||||
|
mach_port_name: failure.mach_port_name,
|
||||||
|
message: failure.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn drain_iosurface_imports(&mut self) -> Result<(), ServoLiveError> {
|
||||||
|
let Some(importer) = self.iosurface_importer.as_ref() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
for result in importer.drain() {
|
||||||
|
match result {
|
||||||
|
IOSurfaceImportResult::Imported(imported) => {
|
||||||
|
self.iosurface_cache
|
||||||
|
.insert_pixel_buffer(imported.surface_id, imported.pixel_buffer);
|
||||||
|
tracing::info!(
|
||||||
|
target: "ely::servo::iosurface",
|
||||||
|
surface_id = imported.surface_id,
|
||||||
|
width = imported.width,
|
||||||
|
height = imported.height,
|
||||||
|
"imported IOSurface into CVPixelBuffer cache",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
IOSurfaceImportResult::Failed(failure) => {
|
||||||
|
tracing::warn!(
|
||||||
|
target: "ely::servo::iosurface",
|
||||||
|
surface_id = failure.surface_id,
|
||||||
|
width = failure.width,
|
||||||
|
height = failure.height,
|
||||||
|
mach_port_name = failure.mach_port_name,
|
||||||
|
message = %failure.message,
|
||||||
|
"IOSurface import worker failed",
|
||||||
|
);
|
||||||
|
return Err(ServoLiveError::IOSurfaceImportFailed {
|
||||||
|
surface_id: failure.surface_id,
|
||||||
|
mach_port_name: failure.mach_port_name,
|
||||||
|
message: failure.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Drop for ServoLiveClient {
|
impl Drop for ServoLiveClient {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let _ = self.child.kill();
|
let _ = self.child.kill();
|
||||||
let _ = self.child.wait();
|
let _ = self.child.wait();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
impl ServoLiveClient {
|
|
||||||
/// Convert the sidecar's `surface_handle` into a `CVPixelBuffer`
|
|
||||||
/// in the local cache. A later frame with a missing pixel buffer
|
|
||||||
/// becomes a web-surface error instead of a blank ready frame.
|
|
||||||
fn import_iosurface_handle(
|
|
||||||
&mut self,
|
|
||||||
handle: &LiveSurfaceHandle,
|
|
||||||
) -> Result<(), ServoLiveError> {
|
|
||||||
let mach_port_name = match self.iosurface_receiver.as_mut() {
|
|
||||||
Some(receiver) => {
|
|
||||||
receiver.receive_port_for_surface(handle.surface_id, Duration::from_secs(1))?
|
|
||||||
}
|
|
||||||
None => handle.mach_port_name,
|
|
||||||
};
|
|
||||||
match self.iosurface_cache.import(mach_port_name, handle.surface_id) {
|
|
||||||
Ok(()) => tracing::info!(
|
|
||||||
target: "ely::servo::iosurface",
|
|
||||||
surface_id = handle.surface_id,
|
|
||||||
width = handle.width,
|
|
||||||
height = handle.height,
|
|
||||||
"imported IOSurface into CVPixelBuffer cache",
|
|
||||||
),
|
|
||||||
Err(error) => {
|
|
||||||
tracing::warn!(
|
|
||||||
target: "ely::servo::iosurface",
|
|
||||||
error = %error,
|
|
||||||
surface_id = handle.surface_id,
|
|
||||||
"IOSurface→CVPixelBuffer import failed",
|
|
||||||
);
|
|
||||||
return Err(ServoLiveError::IOSurfaceImportFailed {
|
|
||||||
surface_id: handle.surface_id,
|
|
||||||
mach_port_name,
|
|
||||||
message: error.to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct ServoLiveEnsureRequest {
|
|
||||||
pub(crate) tab_id: String,
|
|
||||||
pub(crate) profile_id: String,
|
|
||||||
pub(crate) url: String,
|
|
||||||
pub(crate) width: u32,
|
|
||||||
pub(crate) height: u32,
|
|
||||||
pub(crate) page_zoom_percent: u16,
|
|
||||||
/// Display scale factor (1.0 standard, 2.0 Retina). Servo's
|
|
||||||
/// WebView lays out CSS pixels = device pixels / hidpi factor;
|
|
||||||
/// without this, a Retina viewport gets desktop-CSS-pixel layout
|
|
||||||
/// and every visible element renders at half its expected size.
|
|
||||||
pub(crate) device_pixel_ratio: f32,
|
|
||||||
pub(crate) scroll_delta_x: i32,
|
|
||||||
pub(crate) scroll_delta_y: i32,
|
|
||||||
pub(crate) scroll_point_x: Option<u32>,
|
|
||||||
pub(crate) scroll_point_y: Option<u32>,
|
|
||||||
pub(crate) click_x: Option<u32>,
|
|
||||||
pub(crate) click_y: Option<u32>,
|
|
||||||
pub(crate) hover_x: Option<u32>,
|
|
||||||
pub(crate) hover_y: Option<u32>,
|
|
||||||
pub(crate) typed_text: Option<String>,
|
|
||||||
pub(crate) site_permissions: Vec<ServoLiveSitePermission>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
|
||||||
pub(crate) struct ServoLiveSitePermission {
|
|
||||||
pub(crate) origin: String,
|
|
||||||
pub(crate) feature: String,
|
|
||||||
pub(crate) decision: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ServoLiveSitePermission {
|
|
||||||
pub fn new(
|
|
||||||
origin: impl Into<String>,
|
|
||||||
feature: impl Into<String>,
|
|
||||||
decision: SitePermissionDecision,
|
|
||||||
) -> Self {
|
|
||||||
Self { origin: origin.into(), feature: feature.into(), decision: decision.as_str().into() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct ServoLiveFrame {
|
|
||||||
loaded_url: Option<String>,
|
|
||||||
title: Option<String>,
|
|
||||||
render_state: String,
|
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
device_pixel_ratio: f32,
|
|
||||||
css_viewport_width: u32,
|
|
||||||
css_viewport_height: u32,
|
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
|
||||||
non_white_pixel_count: u64,
|
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
|
||||||
content_pixel_count: u64,
|
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
|
||||||
sample_hash: u64,
|
|
||||||
rgba_bytes: Vec<u8>,
|
|
||||||
/// Hardware-path surface: the imported IOSurface published by the
|
|
||||||
/// sidecar, wrapped as a CVPixelBuffer for GPUI's `surface(...)`.
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pixel_buffer: Option<CVPixelBuffer>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// SAFETY: CVPixelBuffer wraps CVPixelBufferRef, a CoreFoundation type
|
|
||||||
// Apple documents as safe to share across threads. The Rust core-video
|
|
||||||
// crate does not mark it Send, so the worker thread needs this opt-in
|
|
||||||
// to ship hardware frames back to the UI thread via mpsc::Sender.
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
#[expect(unsafe_code)]
|
|
||||||
unsafe impl Send for ServoLiveFrame {}
|
|
||||||
|
|
||||||
impl ServoLiveFrame {
|
|
||||||
fn from_parts(report: LiveFrameReport, rgba_bytes: Vec<u8>) -> Self {
|
|
||||||
let (css_viewport_width, css_viewport_height) = css_viewport_size_from_report(&report);
|
|
||||||
Self {
|
|
||||||
loaded_url: report.loaded_url,
|
|
||||||
title: report.title,
|
|
||||||
render_state: report.state,
|
|
||||||
width: report.width,
|
|
||||||
height: report.height,
|
|
||||||
device_pixel_ratio: report.device_pixel_ratio,
|
|
||||||
css_viewport_width,
|
|
||||||
css_viewport_height,
|
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
|
||||||
non_white_pixel_count: report.non_white_pixel_count,
|
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
|
||||||
content_pixel_count: report.content_pixel_count,
|
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
|
||||||
sample_hash: report.sample_hash,
|
|
||||||
rgba_bytes,
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pixel_buffer: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the imported `CVPixelBuffer` matching the frame's
|
|
||||||
/// current hardware surface.
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
#[must_use]
|
|
||||||
pub fn pixel_buffer(&self) -> Option<&CVPixelBuffer> {
|
|
||||||
self.pixel_buffer.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn loaded_url(&self) -> Option<&str> {
|
|
||||||
self.loaded_url.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn title(&self) -> Option<&str> {
|
|
||||||
self.title.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn render_state(&self) -> &str {
|
|
||||||
self.render_state.as_str()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn width(&self) -> u32 {
|
|
||||||
self.width
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn height(&self) -> u32 {
|
|
||||||
self.height
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn device_pixel_ratio(&self) -> f32 {
|
|
||||||
self.device_pixel_ratio
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn css_viewport_width(&self) -> u32 {
|
|
||||||
self.css_viewport_width
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn css_viewport_height(&self) -> u32 {
|
|
||||||
self.css_viewport_height
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
|
||||||
#[must_use]
|
|
||||||
pub fn non_white_pixel_count(&self) -> u64 {
|
|
||||||
self.non_white_pixel_count
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
|
||||||
#[must_use]
|
|
||||||
pub fn content_pixel_count(&self) -> u64 {
|
|
||||||
self.content_pixel_count
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
|
||||||
#[must_use]
|
|
||||||
pub fn sample_hash(&self) -> u64 {
|
|
||||||
self.sample_hash
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn into_rgba_bytes(self) -> Vec<u8> {
|
|
||||||
self.rgba_bytes
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn for_test(width: u32, height: u32, rgba_bytes: Vec<u8>) -> Self {
|
|
||||||
Self {
|
|
||||||
loaded_url: Some("https://example.com/".to_string()),
|
|
||||||
title: Some("Example".to_string()),
|
|
||||||
render_state: "complete".to_string(),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
device_pixel_ratio: 1.0,
|
|
||||||
css_viewport_width: width,
|
|
||||||
css_viewport_height: height,
|
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
|
||||||
non_white_pixel_count: 0,
|
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
|
||||||
content_pixel_count: 0,
|
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
|
||||||
sample_hash: 0,
|
|
||||||
rgba_bytes,
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
pixel_buffer: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(all(test, target_os = "macos"))]
|
|
||||||
pub(crate) fn for_test_with_pixel_buffer(
|
|
||||||
width: u32,
|
|
||||||
height: u32,
|
|
||||||
pixel_buffer: CVPixelBuffer,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
loaded_url: Some("https://example.com/".to_string()),
|
|
||||||
title: Some("Example".to_string()),
|
|
||||||
render_state: "complete".to_string(),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
device_pixel_ratio: 1.0,
|
|
||||||
css_viewport_width: width,
|
|
||||||
css_viewport_height: height,
|
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
|
||||||
non_white_pixel_count: 0,
|
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
|
||||||
content_pixel_count: 0,
|
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
|
||||||
sample_hash: 0,
|
|
||||||
rgba_bytes: Vec::new(),
|
|
||||||
pixel_buffer: Some(pixel_buffer),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn css_viewport_size_from_report(report: &LiveFrameReport) -> (u32, u32) {
|
|
||||||
let dpr = if report.device_pixel_ratio.is_finite() && report.device_pixel_ratio > 0.0 {
|
|
||||||
report.device_pixel_ratio
|
|
||||||
} else {
|
|
||||||
1.0
|
|
||||||
};
|
|
||||||
let fallback_width = ((report.width as f32) / dpr).round().max(1.0) as u32;
|
|
||||||
let fallback_height = ((report.height as f32) / dpr).round().max(1.0) as u32;
|
|
||||||
(
|
|
||||||
if report.css_viewport_width > 0 { report.css_viewport_width } else { fallback_width },
|
|
||||||
if report.css_viewport_height > 0 { report.css_viewport_height } else { fallback_height },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub(crate) enum ServoLiveError {
|
|
||||||
#[error("servo sidecar binary is unavailable at {path}")]
|
|
||||||
SidecarBinaryUnavailable { path: PathBuf },
|
|
||||||
|
|
||||||
#[error("failed to run servo live sidecar: {0}")]
|
|
||||||
Command(#[source] io::Error),
|
|
||||||
|
|
||||||
#[error("servo live sidecar pipe is unavailable: {name}")]
|
|
||||||
PipeUnavailable { name: &'static str },
|
|
||||||
|
|
||||||
#[error("servo live sidecar exited")]
|
|
||||||
SidecarExited,
|
|
||||||
|
|
||||||
#[error("servo live sidecar failed: {message}")]
|
|
||||||
SidecarFailed { message: String },
|
|
||||||
|
|
||||||
#[error("failed to read servo live frame bytes: {0}")]
|
|
||||||
FrameRead(#[source] io::Error),
|
|
||||||
|
|
||||||
#[error(
|
|
||||||
"servo live sidecar advertised {advertised} frame bytes which exceeds \
|
|
||||||
the {width}x{height} pixel budget ({pixel_budget} bytes)"
|
|
||||||
)]
|
|
||||||
FrameBudgetExceeded { advertised: usize, pixel_budget: u64, width: u32, height: u32 },
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
#[error(
|
|
||||||
"servo live IOSurface import failed for surface {surface_id:#x} \
|
|
||||||
mach port 0x{mach_port_name:x}: {message}"
|
|
||||||
)]
|
|
||||||
IOSurfaceImportFailed { surface_id: u64, mach_port_name: u32, message: String },
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
#[error("servo live IOSurface {surface_id:#x} was selected before its pixel buffer import")]
|
|
||||||
IOSurfacePixelBufferMissing { surface_id: u64 },
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
#[error(transparent)]
|
|
||||||
IOSurfaceMach(#[from] IOSurfaceMachError),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
Json(#[from] serde_json::Error),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
SidecarCommand(#[from] SidecarCommandError),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ServoLiveError {
|
|
||||||
pub(crate) fn is_sidecar_process_unusable(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::SidecarExited => true,
|
|
||||||
Self::Command(error) | Self::FrameRead(error) => matches!(
|
|
||||||
error.kind(),
|
|
||||||
io::ErrorKind::BrokenPipe
|
|
||||||
| io::ErrorKind::ConnectionAborted
|
|
||||||
| io::ErrorKind::ConnectionReset
|
|
||||||
| io::ErrorKind::UnexpectedEof
|
|
||||||
),
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
#![cfg(target_os = "macos")]
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
io,
|
||||||
|
sync::mpsc,
|
||||||
|
thread::{self, JoinHandle},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use core_video::pixel_buffer::CVPixelBuffer;
|
||||||
|
|
||||||
|
use crate::services::{
|
||||||
|
iosurface_mach::IOSurfaceMachReceiver, iosurface_metal::import_pixel_buffer_from_mach_port,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::wire::LiveSurfaceHandle;
|
||||||
|
|
||||||
|
const RECEIVE_TIMEOUT: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
|
pub(super) struct IOSurfaceImportWorker {
|
||||||
|
request_tx: Option<mpsc::Sender<LiveSurfaceHandle>>,
|
||||||
|
result_rx: mpsc::Receiver<IOSurfaceImportResult>,
|
||||||
|
thread: Option<JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IOSurfaceImportWorker {
|
||||||
|
pub(super) fn new(receiver: IOSurfaceMachReceiver) -> Result<Self, io::Error> {
|
||||||
|
let (request_tx, request_rx) = mpsc::channel();
|
||||||
|
let (result_tx, result_rx) = mpsc::channel();
|
||||||
|
let thread = thread::Builder::new()
|
||||||
|
.name("ely-iosurface-import".to_string())
|
||||||
|
.spawn(move || run_import_worker(receiver, request_rx, result_tx))?;
|
||||||
|
|
||||||
|
Ok(Self { request_tx: Some(request_tx), result_rx, thread: Some(thread) })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn submit(&self, handle: LiveSurfaceHandle) -> Result<(), IOSurfaceImportFailure> {
|
||||||
|
let Some(request_tx) = self.request_tx.as_ref() else {
|
||||||
|
return Err(IOSurfaceImportFailure::worker_stopped(handle));
|
||||||
|
};
|
||||||
|
request_tx.send(handle).map_err(|error| IOSurfaceImportFailure::worker_stopped(error.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn drain(&self) -> Vec<IOSurfaceImportResult> {
|
||||||
|
let mut results = Vec::new();
|
||||||
|
while let Ok(result) = self.result_rx.try_recv() {
|
||||||
|
results.push(result);
|
||||||
|
}
|
||||||
|
results
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for IOSurfaceImportWorker {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.request_tx.take();
|
||||||
|
if let Some(thread) = self.thread.take() {
|
||||||
|
let _ = thread.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) enum IOSurfaceImportResult {
|
||||||
|
Imported(ImportedIOSurface),
|
||||||
|
Failed(IOSurfaceImportFailure),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct ImportedIOSurface {
|
||||||
|
pub(super) surface_id: u64,
|
||||||
|
pub(super) width: u32,
|
||||||
|
pub(super) height: u32,
|
||||||
|
pub(super) pixel_buffer: CVPixelBuffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: CVPixelBuffer is a CoreFoundation object with atomic
|
||||||
|
// retain/release semantics. This wrapper crosses from the importer
|
||||||
|
// thread to the live worker thread; GPUI presentation already receives
|
||||||
|
// the same handle through ServoLiveFrame's Send contract.
|
||||||
|
#[expect(unsafe_code)]
|
||||||
|
unsafe impl Send for ImportedIOSurface {}
|
||||||
|
|
||||||
|
pub(super) struct IOSurfaceImportFailure {
|
||||||
|
pub(super) surface_id: u64,
|
||||||
|
pub(super) width: u32,
|
||||||
|
pub(super) height: u32,
|
||||||
|
pub(super) mach_port_name: u32,
|
||||||
|
pub(super) message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IOSurfaceImportFailure {
|
||||||
|
fn worker_stopped(handle: LiveSurfaceHandle) -> Self {
|
||||||
|
Self {
|
||||||
|
surface_id: handle.surface_id,
|
||||||
|
width: handle.width,
|
||||||
|
height: handle.height,
|
||||||
|
mach_port_name: handle.mach_port_name,
|
||||||
|
message: "IOSurface import worker stopped".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_import_worker(
|
||||||
|
mut receiver: IOSurfaceMachReceiver,
|
||||||
|
request_rx: mpsc::Receiver<LiveSurfaceHandle>,
|
||||||
|
result_tx: mpsc::Sender<IOSurfaceImportResult>,
|
||||||
|
) {
|
||||||
|
while let Ok(handle) = request_rx.recv() {
|
||||||
|
let result = import_surface_handle(&mut receiver, handle);
|
||||||
|
if result_tx.send(result).is_err() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn import_surface_handle(
|
||||||
|
receiver: &mut IOSurfaceMachReceiver,
|
||||||
|
handle: LiveSurfaceHandle,
|
||||||
|
) -> IOSurfaceImportResult {
|
||||||
|
let mach_port_name = match receiver.receive_port_for_surface(handle.surface_id, RECEIVE_TIMEOUT)
|
||||||
|
{
|
||||||
|
Ok(mach_port_name) => mach_port_name,
|
||||||
|
Err(error) => {
|
||||||
|
return IOSurfaceImportResult::Failed(IOSurfaceImportFailure {
|
||||||
|
surface_id: handle.surface_id,
|
||||||
|
width: handle.width,
|
||||||
|
height: handle.height,
|
||||||
|
mach_port_name: handle.mach_port_name,
|
||||||
|
message: error.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match import_pixel_buffer_from_mach_port(mach_port_name) {
|
||||||
|
Ok(pixel_buffer) => IOSurfaceImportResult::Imported(ImportedIOSurface {
|
||||||
|
surface_id: handle.surface_id,
|
||||||
|
width: handle.width,
|
||||||
|
height: handle.height,
|
||||||
|
pixel_buffer,
|
||||||
|
}),
|
||||||
|
Err(error) => IOSurfaceImportResult::Failed(IOSurfaceImportFailure {
|
||||||
|
surface_id: handle.surface_id,
|
||||||
|
width: handle.width,
|
||||||
|
height: handle.height,
|
||||||
|
mach_port_name,
|
||||||
|
message: error.to_string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
use std::{io, path::PathBuf};
|
||||||
|
|
||||||
|
use ely_domain::SitePermissionDecision;
|
||||||
|
use serde::Serialize;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use super::wire::LiveFrameReport;
|
||||||
|
use crate::services::servo_sidecar_command::SidecarCommandError;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use crate::services::iosurface_mach::IOSurfaceMachError;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
use core_video::pixel_buffer::CVPixelBuffer;
|
||||||
|
|
||||||
|
pub(crate) struct ServoLiveEnsureRequest {
|
||||||
|
pub(crate) tab_id: String,
|
||||||
|
pub(crate) profile_id: String,
|
||||||
|
pub(crate) url: String,
|
||||||
|
pub(crate) width: u32,
|
||||||
|
pub(crate) height: u32,
|
||||||
|
pub(crate) page_zoom_percent: u16,
|
||||||
|
/// Display scale factor (1.0 standard, 2.0 Retina). Servo's
|
||||||
|
/// WebView lays out CSS pixels = device pixels / hidpi factor;
|
||||||
|
/// without this, a Retina viewport gets desktop-CSS-pixel layout
|
||||||
|
/// and every visible element renders at half its expected size.
|
||||||
|
pub(crate) device_pixel_ratio: f32,
|
||||||
|
pub(crate) scroll_delta_x: i32,
|
||||||
|
pub(crate) scroll_delta_y: i32,
|
||||||
|
pub(crate) scroll_point_x: Option<u32>,
|
||||||
|
pub(crate) scroll_point_y: Option<u32>,
|
||||||
|
pub(crate) click_x: Option<u32>,
|
||||||
|
pub(crate) click_y: Option<u32>,
|
||||||
|
pub(crate) hover_x: Option<u32>,
|
||||||
|
pub(crate) hover_y: Option<u32>,
|
||||||
|
pub(crate) typed_text: Option<String>,
|
||||||
|
pub(crate) site_permissions: Vec<ServoLiveSitePermission>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize)]
|
||||||
|
pub(crate) struct ServoLiveSitePermission {
|
||||||
|
pub(crate) origin: String,
|
||||||
|
pub(crate) feature: String,
|
||||||
|
pub(crate) decision: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServoLiveSitePermission {
|
||||||
|
pub fn new(
|
||||||
|
origin: impl Into<String>,
|
||||||
|
feature: impl Into<String>,
|
||||||
|
decision: SitePermissionDecision,
|
||||||
|
) -> Self {
|
||||||
|
Self { origin: origin.into(), feature: feature.into(), decision: decision.as_str().into() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct ServoLiveFrame {
|
||||||
|
loaded_url: Option<String>,
|
||||||
|
title: Option<String>,
|
||||||
|
render_state: String,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
device_pixel_ratio: f32,
|
||||||
|
css_viewport_width: u32,
|
||||||
|
css_viewport_height: u32,
|
||||||
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
|
non_white_pixel_count: u64,
|
||||||
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
|
content_pixel_count: u64,
|
||||||
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
|
sample_hash: u64,
|
||||||
|
rgba_bytes: Vec<u8>,
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pixel_buffer: Option<CVPixelBuffer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: CVPixelBuffer wraps CVPixelBufferRef, a CoreFoundation type
|
||||||
|
// Apple documents as safe to share across threads. The Rust core-video
|
||||||
|
// crate does not mark it Send, so the worker thread needs this opt-in
|
||||||
|
// to ship hardware frames back to the UI thread via mpsc::Sender.
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[expect(unsafe_code)]
|
||||||
|
unsafe impl Send for ServoLiveFrame {}
|
||||||
|
|
||||||
|
impl ServoLiveFrame {
|
||||||
|
pub(super) fn from_parts(report: LiveFrameReport, rgba_bytes: Vec<u8>) -> Self {
|
||||||
|
let (css_viewport_width, css_viewport_height) = css_viewport_size_from_report(&report);
|
||||||
|
Self {
|
||||||
|
loaded_url: report.loaded_url,
|
||||||
|
title: report.title,
|
||||||
|
render_state: report.state,
|
||||||
|
width: report.width,
|
||||||
|
height: report.height,
|
||||||
|
device_pixel_ratio: report.device_pixel_ratio,
|
||||||
|
css_viewport_width,
|
||||||
|
css_viewport_height,
|
||||||
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
|
non_white_pixel_count: report.non_white_pixel_count,
|
||||||
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
|
content_pixel_count: report.content_pixel_count,
|
||||||
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
|
sample_hash: report.sample_hash,
|
||||||
|
rgba_bytes,
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pixel_buffer: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub(super) fn set_pixel_buffer(&mut self, pixel_buffer: Option<CVPixelBuffer>) {
|
||||||
|
self.pixel_buffer = pixel_buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn pixel_buffer(&self) -> Option<&CVPixelBuffer> {
|
||||||
|
self.pixel_buffer.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn loaded_url(&self) -> Option<&str> {
|
||||||
|
self.loaded_url.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn title(&self) -> Option<&str> {
|
||||||
|
self.title.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn render_state(&self) -> &str {
|
||||||
|
self.render_state.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn width(&self) -> u32 {
|
||||||
|
self.width
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn height(&self) -> u32 {
|
||||||
|
self.height
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn device_pixel_ratio(&self) -> f32 {
|
||||||
|
self.device_pixel_ratio
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn css_viewport_width(&self) -> u32 {
|
||||||
|
self.css_viewport_width
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn css_viewport_height(&self) -> u32 {
|
||||||
|
self.css_viewport_height
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
|
#[must_use]
|
||||||
|
pub fn non_white_pixel_count(&self) -> u64 {
|
||||||
|
self.non_white_pixel_count
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
|
#[must_use]
|
||||||
|
pub fn content_pixel_count(&self) -> u64 {
|
||||||
|
self.content_pixel_count
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
|
#[must_use]
|
||||||
|
pub fn sample_hash(&self) -> u64 {
|
||||||
|
self.sample_hash
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_rgba_bytes(self) -> Vec<u8> {
|
||||||
|
self.rgba_bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn for_test(width: u32, height: u32, rgba_bytes: Vec<u8>) -> Self {
|
||||||
|
Self {
|
||||||
|
loaded_url: Some("https://example.com/".to_string()),
|
||||||
|
title: Some("Example".to_string()),
|
||||||
|
render_state: "complete".to_string(),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
device_pixel_ratio: 1.0,
|
||||||
|
css_viewport_width: width,
|
||||||
|
css_viewport_height: height,
|
||||||
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
|
non_white_pixel_count: 0,
|
||||||
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
|
content_pixel_count: 0,
|
||||||
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
|
sample_hash: 0,
|
||||||
|
rgba_bytes,
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pixel_buffer: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, target_os = "macos"))]
|
||||||
|
pub(crate) fn for_test_with_pixel_buffer(
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
pixel_buffer: CVPixelBuffer,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
loaded_url: Some("https://example.com/".to_string()),
|
||||||
|
title: Some("Example".to_string()),
|
||||||
|
render_state: "complete".to_string(),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
device_pixel_ratio: 1.0,
|
||||||
|
css_viewport_width: width,
|
||||||
|
css_viewport_height: height,
|
||||||
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
|
non_white_pixel_count: 0,
|
||||||
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
|
content_pixel_count: 0,
|
||||||
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
|
sample_hash: 0,
|
||||||
|
rgba_bytes: Vec::new(),
|
||||||
|
pixel_buffer: Some(pixel_buffer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn css_viewport_size_from_report(report: &LiveFrameReport) -> (u32, u32) {
|
||||||
|
let dpr = if report.device_pixel_ratio.is_finite() && report.device_pixel_ratio > 0.0 {
|
||||||
|
report.device_pixel_ratio
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
let fallback_width = ((report.width as f32) / dpr).round().max(1.0) as u32;
|
||||||
|
let fallback_height = ((report.height as f32) / dpr).round().max(1.0) as u32;
|
||||||
|
(
|
||||||
|
if report.css_viewport_width > 0 { report.css_viewport_width } else { fallback_width },
|
||||||
|
if report.css_viewport_height > 0 { report.css_viewport_height } else { fallback_height },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub(crate) enum ServoLiveError {
|
||||||
|
#[error("servo sidecar binary is unavailable at {path}")]
|
||||||
|
SidecarBinaryUnavailable { path: PathBuf },
|
||||||
|
|
||||||
|
#[error("failed to run servo live sidecar: {0}")]
|
||||||
|
Command(#[source] io::Error),
|
||||||
|
|
||||||
|
#[error("servo live sidecar pipe is unavailable: {name}")]
|
||||||
|
PipeUnavailable { name: &'static str },
|
||||||
|
|
||||||
|
#[error("servo live sidecar exited")]
|
||||||
|
SidecarExited,
|
||||||
|
|
||||||
|
#[error("servo live sidecar failed: {message}")]
|
||||||
|
SidecarFailed { message: String },
|
||||||
|
|
||||||
|
#[error("failed to read servo live frame bytes: {0}")]
|
||||||
|
FrameRead(#[source] io::Error),
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"servo live sidecar advertised {advertised} frame bytes which exceeds \
|
||||||
|
the {width}x{height} pixel budget ({pixel_budget} bytes)"
|
||||||
|
)]
|
||||||
|
FrameBudgetExceeded { advertised: usize, pixel_budget: u64, width: u32, height: u32 },
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[error(
|
||||||
|
"servo live IOSurface import failed for surface {surface_id:#x} \
|
||||||
|
mach port 0x{mach_port_name:x}: {message}"
|
||||||
|
)]
|
||||||
|
IOSurfaceImportFailed { surface_id: u64, mach_port_name: u32, message: String },
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[error("failed to spawn servo live IOSurface importer: {0}")]
|
||||||
|
IOSurfaceImportWorker(#[source] io::Error),
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[error(transparent)]
|
||||||
|
IOSurfaceMach(#[from] IOSurfaceMachError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Json(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
SidecarCommand(#[from] SidecarCommandError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServoLiveError {
|
||||||
|
pub(crate) fn is_sidecar_process_unusable(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::SidecarExited => true,
|
||||||
|
Self::Command(error) | Self::FrameRead(error) => matches!(
|
||||||
|
error.kind(),
|
||||||
|
io::ErrorKind::BrokenPipe
|
||||||
|
| io::ErrorKind::ConnectionAborted
|
||||||
|
| io::ErrorKind::ConnectionReset
|
||||||
|
| io::ErrorKind::UnexpectedEof
|
||||||
|
),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,9 +23,11 @@ pub(super) enum LiveRequest {
|
|||||||
hover_y: Option<u32>,
|
hover_y: Option<u32>,
|
||||||
typed_text: Option<String>,
|
typed_text: Option<String>,
|
||||||
site_permissions: Vec<ServoLiveSitePermission>,
|
site_permissions: Vec<ServoLiveSitePermission>,
|
||||||
|
ready_surface_ids: Vec<u64>,
|
||||||
},
|
},
|
||||||
Poll {
|
Poll {
|
||||||
tab_id: String,
|
tab_id: String,
|
||||||
|
ready_surface_ids: Vec<u64>,
|
||||||
},
|
},
|
||||||
Close {
|
Close {
|
||||||
tab_id: String,
|
tab_id: String,
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ use super::perf::{FramePerfAggregator, FramePerfSummary, elapsed_ns};
|
|||||||
|
|
||||||
pub(super) fn run_live(args: LiveArgs) -> Result<(), LiveSidecarError> {
|
pub(super) fn run_live(args: LiveArgs) -> Result<(), LiveSidecarError> {
|
||||||
let LiveArgs { profile_data_dir, iosurface_mach_service, rendering_context_kind } = args;
|
let LiveArgs { profile_data_dir, iosurface_mach_service, rendering_context_kind } = args;
|
||||||
let publish_readback_surface_fields = iosurface_mach_service.is_none();
|
let publish_readback_surface_fields = true;
|
||||||
|
let require_client_ready_surfaces = iosurface_mach_service.is_some();
|
||||||
fs::create_dir_all(&profile_data_dir)?;
|
fs::create_dir_all(&profile_data_dir)?;
|
||||||
let context_label = rendering_context_label(rendering_context_kind);
|
let context_label = rendering_context_label(rendering_context_kind);
|
||||||
let mut host = SoftwareServoHost::new_with_config_dir_and_kind(
|
let mut host = SoftwareServoHost::new_with_config_dir_and_kind(
|
||||||
@@ -66,6 +67,7 @@ pub(super) fn run_live(args: LiveArgs) -> Result<(), LiveSidecarError> {
|
|||||||
&mut published_surface_ids,
|
&mut published_surface_ids,
|
||||||
rendering_context_kind,
|
rendering_context_kind,
|
||||||
publish_readback_surface_fields,
|
publish_readback_surface_fields,
|
||||||
|
require_client_ready_surfaces,
|
||||||
request,
|
request,
|
||||||
),
|
),
|
||||||
Err(error) => Err(LiveSidecarError::Json(error)),
|
Err(error) => Err(LiveSidecarError::Json(error)),
|
||||||
@@ -95,6 +97,7 @@ fn handle_request(
|
|||||||
published_surface_ids: &mut HashMap<String, HashSet<IOSurfaceIdentity>>,
|
published_surface_ids: &mut HashMap<String, HashSet<IOSurfaceIdentity>>,
|
||||||
rendering_context_kind: RenderingContextKind,
|
rendering_context_kind: RenderingContextKind,
|
||||||
publish_readback_surface_fields: bool,
|
publish_readback_surface_fields: bool,
|
||||||
|
require_client_ready_surfaces: bool,
|
||||||
request: LiveRequest,
|
request: LiveRequest,
|
||||||
) -> Result<LiveOutcome, LiveSidecarError> {
|
) -> Result<LiveOutcome, LiveSidecarError> {
|
||||||
match request {
|
match request {
|
||||||
@@ -116,6 +119,7 @@ fn handle_request(
|
|||||||
hover_y,
|
hover_y,
|
||||||
typed_text,
|
typed_text,
|
||||||
site_permissions,
|
site_permissions,
|
||||||
|
ready_surface_ids,
|
||||||
} => {
|
} => {
|
||||||
let tab = TabId::parse(tab_id.clone())?;
|
let tab = TabId::parse(tab_id.clone())?;
|
||||||
let profile = ProfileId::parse(profile_id)?;
|
let profile = ProfileId::parse(profile_id)?;
|
||||||
@@ -162,8 +166,17 @@ fn handle_request(
|
|||||||
session.awaiting_visible_frame = true;
|
session.awaiting_visible_frame = true;
|
||||||
}
|
}
|
||||||
let webview_id = session.webview_id.clone();
|
let webview_id = session.webview_id.clone();
|
||||||
let mut outcome =
|
let mut outcome = poll_frame(
|
||||||
poll_frame(host, session, rendering_context_kind, &tab_id, published_surface_ids)?;
|
host,
|
||||||
|
session,
|
||||||
|
rendering_context_kind,
|
||||||
|
payloadless_readiness(
|
||||||
|
&tab_id,
|
||||||
|
published_surface_ids,
|
||||||
|
&ready_surface_ids,
|
||||||
|
require_client_ready_surfaces,
|
||||||
|
),
|
||||||
|
)?;
|
||||||
populate_surface_fields(
|
populate_surface_fields(
|
||||||
host,
|
host,
|
||||||
&webview_id,
|
&webview_id,
|
||||||
@@ -174,13 +187,22 @@ fn handle_request(
|
|||||||
);
|
);
|
||||||
Ok(outcome)
|
Ok(outcome)
|
||||||
}
|
}
|
||||||
LiveRequest::Poll { tab_id } => {
|
LiveRequest::Poll { tab_id, ready_surface_ids } => {
|
||||||
let Some(session) = sessions.get_mut(&tab_id) else {
|
let Some(session) = sessions.get_mut(&tab_id) else {
|
||||||
return Ok(LiveOutcome::empty());
|
return Ok(LiveOutcome::empty());
|
||||||
};
|
};
|
||||||
let webview_id = session.webview_id.clone();
|
let webview_id = session.webview_id.clone();
|
||||||
let mut outcome =
|
let mut outcome = poll_frame(
|
||||||
poll_frame(host, session, rendering_context_kind, &tab_id, published_surface_ids)?;
|
host,
|
||||||
|
session,
|
||||||
|
rendering_context_kind,
|
||||||
|
payloadless_readiness(
|
||||||
|
&tab_id,
|
||||||
|
published_surface_ids,
|
||||||
|
&ready_surface_ids,
|
||||||
|
require_client_ready_surfaces,
|
||||||
|
),
|
||||||
|
)?;
|
||||||
populate_surface_fields(
|
populate_surface_fields(
|
||||||
host,
|
host,
|
||||||
&webview_id,
|
&webview_id,
|
||||||
@@ -205,8 +227,7 @@ fn poll_frame(
|
|||||||
host: &mut SoftwareServoHost,
|
host: &mut SoftwareServoHost,
|
||||||
session: &mut LiveSession,
|
session: &mut LiveSession,
|
||||||
rendering_context_kind: RenderingContextKind,
|
rendering_context_kind: RenderingContextKind,
|
||||||
tab_id: &str,
|
readiness: PayloadlessReadiness<'_>,
|
||||||
published_surface_ids: &HashMap<String, HashSet<IOSurfaceIdentity>>,
|
|
||||||
) -> Result<LiveOutcome, LiveSidecarError> {
|
) -> Result<LiveOutcome, LiveSidecarError> {
|
||||||
host.tick();
|
host.tick();
|
||||||
let snapshot = host.snapshot(&session.webview_id)?;
|
let snapshot = host.snapshot(&session.webview_id)?;
|
||||||
@@ -215,14 +236,8 @@ fn poll_frame(
|
|||||||
return Ok(LiveOutcome::empty());
|
return Ok(LiveOutcome::empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
let (outcome, has_visible_content) = paint_pending_frame(
|
let (outcome, has_visible_content) =
|
||||||
host,
|
paint_pending_frame(host, session, rendering_context_kind, readiness, has_pending_frame)?;
|
||||||
session,
|
|
||||||
rendering_context_kind,
|
|
||||||
tab_id,
|
|
||||||
published_surface_ids,
|
|
||||||
has_pending_frame,
|
|
||||||
)?;
|
|
||||||
if has_visible_content {
|
if has_visible_content {
|
||||||
session.awaiting_visible_frame = false;
|
session.awaiting_visible_frame = false;
|
||||||
session.ever_visible_frame = true;
|
session.ever_visible_frame = true;
|
||||||
@@ -243,23 +258,18 @@ fn paint_pending_frame(
|
|||||||
host: &mut SoftwareServoHost,
|
host: &mut SoftwareServoHost,
|
||||||
session: &mut LiveSession,
|
session: &mut LiveSession,
|
||||||
rendering_context_kind: RenderingContextKind,
|
rendering_context_kind: RenderingContextKind,
|
||||||
tab_id: &str,
|
readiness: PayloadlessReadiness<'_>,
|
||||||
published_surface_ids: &HashMap<String, HashSet<IOSurfaceIdentity>>,
|
|
||||||
has_pending_frame: bool,
|
has_pending_frame: bool,
|
||||||
) -> Result<(LiveOutcome, bool), LiveSidecarError> {
|
) -> Result<(LiveOutcome, bool), LiveSidecarError> {
|
||||||
#[cfg(not(all(feature = "hardware-render", target_os = "macos")))]
|
#[cfg(not(all(feature = "hardware-render", target_os = "macos")))]
|
||||||
let _ = (tab_id, published_surface_ids);
|
let _ = readiness;
|
||||||
|
|
||||||
match rendering_context_kind {
|
match rendering_context_kind {
|
||||||
RenderingContextKind::Software => paint_readback_frame(host, session, !has_pending_frame),
|
RenderingContextKind::Software => paint_readback_frame(host, session, !has_pending_frame),
|
||||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
RenderingContextKind::Hardware => paint_hardware_surface_frame(
|
RenderingContextKind::Hardware => {
|
||||||
host,
|
paint_hardware_surface_frame(host, session, readiness, has_pending_frame)
|
||||||
session,
|
}
|
||||||
tab_id,
|
|
||||||
published_surface_ids,
|
|
||||||
has_pending_frame,
|
|
||||||
),
|
|
||||||
#[cfg(not(all(feature = "hardware-render", target_os = "macos")))]
|
#[cfg(not(all(feature = "hardware-render", target_os = "macos")))]
|
||||||
RenderingContextKind::Hardware => paint_readback_frame(host, session, !has_pending_frame),
|
RenderingContextKind::Hardware => paint_readback_frame(host, session, !has_pending_frame),
|
||||||
}
|
}
|
||||||
@@ -288,19 +298,17 @@ fn paint_readback_frame(
|
|||||||
fn paint_hardware_surface_frame(
|
fn paint_hardware_surface_frame(
|
||||||
host: &mut SoftwareServoHost,
|
host: &mut SoftwareServoHost,
|
||||||
session: &LiveSession,
|
session: &LiveSession,
|
||||||
tab_id: &str,
|
readiness: PayloadlessReadiness<'_>,
|
||||||
published_surface_ids: &HashMap<String, HashSet<IOSurfaceIdentity>>,
|
|
||||||
has_pending_frame: bool,
|
has_pending_frame: bool,
|
||||||
) -> Result<(LiveOutcome, bool), LiveSidecarError> {
|
) -> Result<(LiveOutcome, bool), LiveSidecarError> {
|
||||||
if !session.ever_visible_frame {
|
if !session.ever_visible_frame {
|
||||||
return paint_initial_hardware_surface_frame(host, session, !has_pending_frame);
|
return paint_initial_hardware_surface_frame(host, session, !has_pending_frame);
|
||||||
}
|
}
|
||||||
if !payloadless_surface_pool_ready(published_surface_ids, tab_id, session.width, session.height)
|
if !payloadless_surface_pool_ready(readiness, session.width, session.height) {
|
||||||
{
|
|
||||||
return paint_readback_frame(host, session, !has_pending_frame);
|
return paint_readback_frame(host, session, !has_pending_frame);
|
||||||
}
|
}
|
||||||
let (outcome, identity) = paint_hardware_surface_report(host, session, !has_pending_frame)?;
|
let (outcome, identity) = paint_hardware_surface_report(host, session, !has_pending_frame)?;
|
||||||
if !surface_has_been_published(published_surface_ids, tab_id, identity) {
|
if !surface_has_been_published(readiness.published_surface_ids, readiness.tab_id, identity) {
|
||||||
return paint_readback_frame(host, session, true);
|
return paint_readback_frame(host, session, true);
|
||||||
}
|
}
|
||||||
Ok((outcome, true))
|
Ok((outcome, true))
|
||||||
@@ -352,19 +360,62 @@ fn paint_hardware_surface_report(
|
|||||||
|
|
||||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
fn payloadless_surface_pool_ready(
|
fn payloadless_surface_pool_ready(
|
||||||
published_surface_ids: &HashMap<String, HashSet<IOSurfaceIdentity>>,
|
readiness: PayloadlessReadiness<'_>,
|
||||||
tab_id: &str,
|
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
published_surface_ids.get(tab_id).is_some_and(|published| {
|
let Some(published) = readiness.published_surface_ids.get(readiness.tab_id) else {
|
||||||
published
|
return false;
|
||||||
|
};
|
||||||
|
let matching = published
|
||||||
|
.iter()
|
||||||
|
.filter(|identity| identity.width == width && identity.height == height)
|
||||||
|
.take(2)
|
||||||
|
.copied()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if matching.len() < 2 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
!readiness.require_client_ready_surfaces
|
||||||
|
|| matching
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|identity| identity.width == width && identity.height == height)
|
.all(|identity| readiness.ready_surface_ids.contains(&identity.surface_id))
|
||||||
.take(2)
|
}
|
||||||
.count()
|
|
||||||
>= 2
|
#[derive(Clone, Copy)]
|
||||||
})
|
struct PayloadlessReadiness<'a> {
|
||||||
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
|
tab_id: &'a str,
|
||||||
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
|
published_surface_ids: &'a HashMap<String, HashSet<IOSurfaceIdentity>>,
|
||||||
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
|
ready_surface_ids: &'a [u64],
|
||||||
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
|
require_client_ready_surfaces: bool,
|
||||||
|
#[cfg(not(all(feature = "hardware-render", target_os = "macos")))]
|
||||||
|
_marker: std::marker::PhantomData<&'a ()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn payloadless_readiness<'a>(
|
||||||
|
tab_id: &'a str,
|
||||||
|
published_surface_ids: &'a HashMap<String, HashSet<IOSurfaceIdentity>>,
|
||||||
|
ready_surface_ids: &'a [u64],
|
||||||
|
require_client_ready_surfaces: bool,
|
||||||
|
) -> PayloadlessReadiness<'a> {
|
||||||
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
|
{
|
||||||
|
PayloadlessReadiness {
|
||||||
|
tab_id,
|
||||||
|
published_surface_ids,
|
||||||
|
ready_surface_ids,
|
||||||
|
require_client_ready_surfaces,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(all(feature = "hardware-render", target_os = "macos")))]
|
||||||
|
{
|
||||||
|
let _ = (tab_id, published_surface_ids, ready_surface_ids, require_client_ready_surfaces);
|
||||||
|
PayloadlessReadiness { _marker: std::marker::PhantomData }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
@@ -390,4 +441,49 @@ mod tests {
|
|||||||
assert!(!should_paint_live_frame(false, false));
|
assert!(!should_paint_live_frame(false, false));
|
||||||
assert!(should_paint_live_frame(true, false));
|
assert!(should_paint_live_frame(true, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
|
#[test]
|
||||||
|
fn payloadless_pool_waits_for_client_ready_surfaces_when_required() {
|
||||||
|
let published = published_identities([identity(7, 800, 600), identity(8, 800, 600)]);
|
||||||
|
|
||||||
|
assert!(!payloadless_surface_pool_ready(readiness(&published, &[7], true), 800, 600));
|
||||||
|
assert!(payloadless_surface_pool_ready(readiness(&published, &[7, 8], true), 800, 600));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
|
#[test]
|
||||||
|
fn payloadless_pool_uses_published_surfaces_for_no_mach_clients() {
|
||||||
|
let published = published_identities([identity(7, 800, 600), identity(8, 800, 600)]);
|
||||||
|
|
||||||
|
assert!(payloadless_surface_pool_ready(readiness(&published, &[], false), 800, 600));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
|
fn readiness<'a>(
|
||||||
|
published_surface_ids: &'a HashMap<String, HashSet<IOSurfaceIdentity>>,
|
||||||
|
ready_surface_ids: &'a [u64],
|
||||||
|
require_client_ready_surfaces: bool,
|
||||||
|
) -> PayloadlessReadiness<'a> {
|
||||||
|
PayloadlessReadiness {
|
||||||
|
tab_id: "tab",
|
||||||
|
published_surface_ids,
|
||||||
|
ready_surface_ids,
|
||||||
|
require_client_ready_surfaces,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
|
fn published_identities(
|
||||||
|
identities: [IOSurfaceIdentity; 2],
|
||||||
|
) -> HashMap<String, HashSet<IOSurfaceIdentity>> {
|
||||||
|
let mut published = HashMap::new();
|
||||||
|
published.insert("tab".to_string(), identities.into_iter().collect());
|
||||||
|
published
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||||
|
fn identity(surface_id: u64, width: u32, height: u32) -> IOSurfaceIdentity {
|
||||||
|
IOSurfaceIdentity { surface_id, width, height }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,10 @@ use ely_servo_host::{IOSurfaceIdentity, SoftwareServoHost};
|
|||||||
use super::live_protocol::{LiveOutcome, LiveSidecarError, PartialFrameTimings};
|
use super::live_protocol::{LiveOutcome, LiveSidecarError, PartialFrameTimings};
|
||||||
use super::perf::{FramePerfAggregator, FramePerfSummary, FrameStageTimings, elapsed_ns};
|
use super::perf::{FramePerfAggregator, FramePerfSummary, FrameStageTimings, elapsed_ns};
|
||||||
|
|
||||||
/// Populate the hardware surface protocol fields on `outcome`. Mach
|
/// Populate the hardware surface protocol fields on `outcome`. Readback
|
||||||
/// app clients keep readback frames free of surface fields because
|
/// warm-up frames publish IOSurface handles so the app can import them
|
||||||
/// synchronous IOSurface import can block the live worker; the no-Mach
|
/// on its dedicated importer thread before steady-state payloadless
|
||||||
/// bench path publishes readback warm-up handles so it can validate
|
/// frames select the rotating surface ids. Two pieces of state ride out
|
||||||
/// payloadless steady-state frames. Two pieces of state ride out
|
|
||||||
/// together:
|
/// together:
|
||||||
///
|
///
|
||||||
/// * `current_surface_id` — set on every payload-bearing hardware
|
/// * `current_surface_id` — set on every payload-bearing hardware
|
||||||
|
|||||||
@@ -43,9 +43,13 @@ pub(super) enum LiveRequest {
|
|||||||
hover_y: Option<u32>,
|
hover_y: Option<u32>,
|
||||||
typed_text: Option<String>,
|
typed_text: Option<String>,
|
||||||
site_permissions: Vec<LiveSitePermission>,
|
site_permissions: Vec<LiveSitePermission>,
|
||||||
|
#[serde(default)]
|
||||||
|
ready_surface_ids: Vec<u64>,
|
||||||
},
|
},
|
||||||
Poll {
|
Poll {
|
||||||
tab_id: String,
|
tab_id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
ready_surface_ids: Vec<u64>,
|
||||||
},
|
},
|
||||||
Close {
|
Close {
|
||||||
tab_id: String,
|
tab_id: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user