Skip hardware live readback
This commit is contained in:
@@ -353,48 +353,8 @@ fn poll_frame(
|
||||
host.tick();
|
||||
let snapshot = host.snapshot(&session.webview_id)?;
|
||||
if snapshot.has_pending_frame() {
|
||||
let paint_started_at = Instant::now();
|
||||
host.paint(&session.webview_id)?;
|
||||
let snapshot = host.snapshot(&session.webview_id)?;
|
||||
let frame = host.last_rendered_frame()?;
|
||||
let paint_ns = elapsed_ns(paint_started_at);
|
||||
let encode_started_at = Instant::now();
|
||||
// `non_white`/`content_pixel_count` come from a CPU
|
||||
// readback of the bound framebuffer. On the software
|
||||
// path that's the source of truth: Servo's compositor
|
||||
// returns a white framebuffer until layout completes, so
|
||||
// the threshold check is how we skip blank loading
|
||||
// frames. On the hardware path the readback goes through
|
||||
// `glReadPixels` on the surfman swap-chain surface, which
|
||||
// can return content that fails the threshold even when
|
||||
// Servo painted real pixels (the IOSurface itself is
|
||||
// valid for GPUI to sample directly). `has_pending_frame`
|
||||
// already encodes "Servo finished painting" — trust it
|
||||
// for the hardware path instead of double-checking via a
|
||||
// readback we know to be unreliable.
|
||||
//
|
||||
// For the software path, the threshold check is only
|
||||
// useful once per URL: after we've confirmed at least one
|
||||
// paint had real content, every subsequent input deserves
|
||||
// an immediate return on the next pending frame instead
|
||||
// of another 250 ms wait. `session.ever_visible_frame`
|
||||
// tracks that, reset on navigate.
|
||||
let has_visible_content = match rendering_context_kind {
|
||||
RenderingContextKind::Software => {
|
||||
session.ever_visible_frame
|
||||
|| (frame.non_white_pixel_count() > 0 && frame.content_pixel_count() > 0)
|
||||
}
|
||||
#[cfg(feature = "hardware-render")]
|
||||
RenderingContextKind::Hardware => true,
|
||||
#[cfg(not(feature = "hardware-render"))]
|
||||
RenderingContextKind::Hardware => {
|
||||
frame.non_white_pixel_count() > 0 && frame.content_pixel_count() > 0
|
||||
}
|
||||
};
|
||||
let report = LiveFrameReport::new(&snapshot, &frame);
|
||||
let encode_ns = elapsed_ns(encode_started_at);
|
||||
let timings = PartialFrameTimings { paint_ns, encode_ns };
|
||||
let outcome = LiveOutcome::from_frame(report, frame, timings);
|
||||
let (outcome, has_visible_content) =
|
||||
paint_pending_frame(host, session, rendering_context_kind)?;
|
||||
if has_visible_content {
|
||||
session.awaiting_visible_frame = false;
|
||||
session.ever_visible_frame = true;
|
||||
@@ -417,6 +377,54 @@ fn poll_frame(
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_pending_frame(
|
||||
host: &mut SoftwareServoHost,
|
||||
session: &mut LiveSession,
|
||||
rendering_context_kind: RenderingContextKind,
|
||||
) -> Result<(LiveOutcome, bool), LiveSidecarError> {
|
||||
match rendering_context_kind {
|
||||
RenderingContextKind::Software => paint_readback_frame(host, session),
|
||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||
RenderingContextKind::Hardware => paint_hardware_surface_frame(host, session),
|
||||
#[cfg(not(all(feature = "hardware-render", target_os = "macos")))]
|
||||
RenderingContextKind::Hardware => paint_readback_frame(host, session),
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_readback_frame(
|
||||
host: &mut SoftwareServoHost,
|
||||
session: &LiveSession,
|
||||
) -> Result<(LiveOutcome, bool), LiveSidecarError> {
|
||||
let paint_started_at = Instant::now();
|
||||
host.paint(&session.webview_id)?;
|
||||
let snapshot = host.snapshot(&session.webview_id)?;
|
||||
let frame = host.last_rendered_frame()?;
|
||||
let paint_ns = elapsed_ns(paint_started_at);
|
||||
let encode_started_at = Instant::now();
|
||||
let has_visible_content = session.ever_visible_frame
|
||||
|| (frame.non_white_pixel_count() > 0 && frame.content_pixel_count() > 0);
|
||||
let report = LiveFrameReport::new(&snapshot, &frame);
|
||||
let encode_ns = elapsed_ns(encode_started_at);
|
||||
let timings = PartialFrameTimings { paint_ns, encode_ns };
|
||||
Ok((LiveOutcome::from_frame(report, frame, timings), has_visible_content))
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||
fn paint_hardware_surface_frame(
|
||||
host: &mut SoftwareServoHost,
|
||||
session: &LiveSession,
|
||||
) -> Result<(LiveOutcome, bool), LiveSidecarError> {
|
||||
let paint_started_at = Instant::now();
|
||||
host.paint_without_readback(&session.webview_id)?;
|
||||
let snapshot = host.snapshot(&session.webview_id)?;
|
||||
let paint_ns = elapsed_ns(paint_started_at);
|
||||
let encode_started_at = Instant::now();
|
||||
let report = LiveFrameReport::new_hardware_surface(&snapshot, session.width, session.height);
|
||||
let encode_ns = elapsed_ns(encode_started_at);
|
||||
let timings = PartialFrameTimings { paint_ns, encode_ns };
|
||||
Ok((LiveOutcome::from_report(report, timings), true))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct LiveSession {
|
||||
webview_id: ely_domain::WebViewId,
|
||||
|
||||
@@ -32,7 +32,7 @@ pub(super) fn populate_surface_fields(
|
||||
published_surface_ids: &mut HashMap<String, HashSet<u64>>,
|
||||
outcome: &mut LiveOutcome,
|
||||
) {
|
||||
if outcome.frame.is_none() {
|
||||
if outcome.response.frame.is_none() {
|
||||
return;
|
||||
}
|
||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||
@@ -124,7 +124,7 @@ pub(super) fn write_outcome(
|
||||
) -> Result<(), LiveSidecarError> {
|
||||
let mut outcome = outcome.unwrap_or_else(|error| LiveOutcome::error(error.to_string()));
|
||||
let partial_timings = outcome.partial_timings.take();
|
||||
let frame_present = outcome.frame.is_some();
|
||||
let frame_present = outcome.response.frame.is_some();
|
||||
if let Some(summary) = pending_summary.take() {
|
||||
outcome.response.perf = Some(summary);
|
||||
}
|
||||
@@ -163,11 +163,19 @@ pub(super) fn write_outcome(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
error::Error,
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use ely_servo_host::{IOSurfaceHandle, IOSurfaceIdentity};
|
||||
|
||||
use super::surface_publication_for;
|
||||
use super::super::{
|
||||
live_protocol::{LiveFrameReport, LiveOutcome, PartialFrameTimings},
|
||||
perf::FramePerfAggregator,
|
||||
};
|
||||
use super::{surface_publication_for, write_outcome};
|
||||
|
||||
#[test]
|
||||
fn unpublished_surface_without_handle_leaves_selector_empty() {
|
||||
@@ -217,6 +225,35 @@ mod tests {
|
||||
assert!(published.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn payloadless_surface_report_records_perf_and_writes_no_rgba() -> Result<(), Box<dyn Error>> {
|
||||
let mut outcome = LiveOutcome::from_report(
|
||||
report_with_byte_count(16),
|
||||
PartialFrameTimings { paint_ns: 1_000, encode_ns: 2_000 },
|
||||
);
|
||||
outcome.response.current_surface_id = Some(7);
|
||||
let mut stdout = Vec::new();
|
||||
let mut perf = FramePerfAggregator::new("hardware", 1);
|
||||
let mut pending_summary = None;
|
||||
|
||||
write_outcome(&mut stdout, &mut perf, &mut pending_summary, Ok(outcome), Instant::now())?;
|
||||
|
||||
let Some(newline_index) = stdout.iter().position(|byte| *byte == b'\n') else {
|
||||
return Err("response newline missing".into());
|
||||
};
|
||||
let line = std::str::from_utf8(&stdout[..newline_index])?;
|
||||
let response: serde_json::Value = serde_json::from_str(line)?;
|
||||
let rgba_byte_count = response
|
||||
.get("frame")
|
||||
.and_then(|frame| frame.get("rgba_byte_count"))
|
||||
.and_then(serde_json::Value::as_u64);
|
||||
|
||||
assert_eq!(rgba_byte_count, Some(0));
|
||||
assert!(stdout[newline_index + 1..].is_empty());
|
||||
assert!(pending_summary.is_some());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn identity(surface_id: u64, width: u32, height: u32) -> IOSurfaceIdentity {
|
||||
IOSurfaceIdentity { surface_id, width, height }
|
||||
}
|
||||
@@ -224,4 +261,18 @@ mod tests {
|
||||
fn handle(surface_id: u64, width: u32, height: u32) -> IOSurfaceHandle {
|
||||
IOSurfaceHandle { mach_port_name: 42, surface_id, width, height }
|
||||
}
|
||||
|
||||
fn report_with_byte_count(rgba_byte_count: usize) -> LiveFrameReport {
|
||||
LiveFrameReport {
|
||||
loaded_url: Some("https://example.com/".to_string()),
|
||||
title: Some("Example".to_string()),
|
||||
state: "complete",
|
||||
width: 2,
|
||||
height: 2,
|
||||
rgba_byte_count,
|
||||
non_white_pixel_count: 0,
|
||||
content_pixel_count: 0,
|
||||
sample_hash: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,10 +67,10 @@ pub(super) struct PartialFrameTimings {
|
||||
pub encode_ns: u64,
|
||||
}
|
||||
|
||||
/// A handle plus an optional rendered frame and partial stage
|
||||
/// timings. We hold the `RenderedFrame` so the write step can stream
|
||||
/// its existing rgba slice straight onto the pipe — no extra
|
||||
/// `to_vec()`.
|
||||
/// A response plus an optional software RGBA payload and partial stage
|
||||
/// timings. Software frames carry `RenderedFrame` so the write step
|
||||
/// can stream its existing rgba slice straight onto the pipe; hardware
|
||||
/// surface frames carry only a `LiveFrameReport`.
|
||||
pub(super) struct LiveOutcome {
|
||||
pub response: LiveResponse,
|
||||
pub frame: Option<RenderedFrame>,
|
||||
@@ -97,6 +97,15 @@ impl LiveOutcome {
|
||||
partial_timings: Some(partial_timings),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, all(feature = "hardware-render", target_os = "macos")))]
|
||||
pub fn from_report(report: LiveFrameReport, partial_timings: PartialFrameTimings) -> Self {
|
||||
Self {
|
||||
response: LiveResponse::frame(report),
|
||||
frame: None,
|
||||
partial_timings: Some(partial_timings),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -182,6 +191,23 @@ impl LiveFrameReport {
|
||||
sample_hash: frame.sample_hash(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a report for the hardware IOSurface path. Pixel metrics
|
||||
/// are unavailable because the path skips framebuffer readback.
|
||||
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
|
||||
pub fn new_hardware_surface(snapshot: &WebViewSnapshot, width: u32, height: u32) -> Self {
|
||||
Self {
|
||||
loaded_url: snapshot.url().map(str::to_string),
|
||||
title: snapshot.title().map(str::to_string),
|
||||
state: state_label(snapshot.state()),
|
||||
width,
|
||||
height,
|
||||
rgba_byte_count: 0,
|
||||
non_white_pixel_count: 0,
|
||||
content_pixel_count: 0,
|
||||
sample_hash: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn state_label(state: &WebViewState) -> &'static str {
|
||||
|
||||
Reference in New Issue
Block a user