T10.7: bypass visible-content gate on hardware path (196ms → 6ms per frame)

This commit is contained in:
2026-05-11 00:05:04 -04:00
parent 6e2bd20a9b
commit d570b84c9c
@@ -28,11 +28,12 @@ const LIVE_FRAME_WAIT_INTERVAL: Duration = Duration::from_millis(2);
pub(super) fn run_live(args: LiveArgs) -> Result<(), LiveSidecarError> { pub(super) fn run_live(args: LiveArgs) -> Result<(), LiveSidecarError> {
fs::create_dir_all(&args.profile_data_dir)?; fs::create_dir_all(&args.profile_data_dir)?;
let context_label = rendering_context_label(args.rendering_context_kind); let rendering_context_kind = args.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(
ServoSurfaceSize::new(1, 1), ServoSurfaceSize::new(1, 1),
Some(args.profile_data_dir), Some(args.profile_data_dir),
args.rendering_context_kind, rendering_context_kind,
)?; )?;
let mut sessions = HashMap::new(); let mut sessions = HashMap::new();
let mut perf = let mut perf =
@@ -55,9 +56,13 @@ pub(super) fn run_live(args: LiveArgs) -> Result<(), LiveSidecarError> {
// `write_outcome`. // `write_outcome`.
let frame_started_at = Instant::now(); let frame_started_at = Instant::now();
let outcome = match serde_json::from_str::<LiveRequest>(&line) { let outcome = match serde_json::from_str::<LiveRequest>(&line) {
Ok(request) => { Ok(request) => handle_request(
handle_request(&mut host, &mut sessions, &mut published_surface_ids, request) &mut host,
} &mut sessions,
&mut published_surface_ids,
rendering_context_kind,
request,
),
Err(error) => Err(LiveSidecarError::Json(error)), Err(error) => Err(LiveSidecarError::Json(error)),
}; };
write_outcome( write_outcome(
@@ -83,6 +88,7 @@ fn handle_request(
host: &mut SoftwareServoHost, host: &mut SoftwareServoHost,
sessions: &mut HashMap<String, LiveSession>, sessions: &mut HashMap<String, LiveSession>,
published_surface_ids: &mut HashMap<String, HashSet<u64>>, published_surface_ids: &mut HashMap<String, HashSet<u64>>,
rendering_context_kind: RenderingContextKind,
request: LiveRequest, request: LiveRequest,
) -> Result<LiveOutcome, LiveSidecarError> { ) -> Result<LiveOutcome, LiveSidecarError> {
match request { match request {
@@ -134,10 +140,16 @@ fn handle_request(
hover_y, hover_y,
typed_text, typed_text,
)? { )? {
// Tell poll_frame to actually wait for Servo to paint
// a response to this input. The visible-content gate
// is bypassed on the hardware path inside poll_frame,
// so we return on the first `has_pending_frame=true`
// (~3 ms in practice) rather than burning the full
// LIVE_FRAME_WAIT_TIMEOUT.
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 = poll_frame(host, session)?; let mut outcome = poll_frame(host, session, rendering_context_kind)?;
populate_surface_fields(host, &webview_id, &tab_id, published_surface_ids, &mut outcome); populate_surface_fields(host, &webview_id, &tab_id, published_surface_ids, &mut outcome);
Ok(outcome) Ok(outcome)
} }
@@ -146,7 +158,7 @@ fn handle_request(
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 = poll_frame(host, session)?; let mut outcome = poll_frame(host, session, rendering_context_kind)?;
populate_surface_fields(host, &webview_id, &tab_id, published_surface_ids, &mut outcome); populate_surface_fields(host, &webview_id, &tab_id, published_surface_ids, &mut outcome);
Ok(outcome) Ok(outcome)
} }
@@ -377,6 +389,7 @@ fn apply_input(
fn poll_frame( fn poll_frame(
host: &mut SoftwareServoHost, host: &mut SoftwareServoHost,
session: &mut LiveSession, session: &mut LiveSession,
rendering_context_kind: RenderingContextKind,
) -> Result<LiveOutcome, LiveSidecarError> { ) -> Result<LiveOutcome, LiveSidecarError> {
let started_at = Instant::now(); let started_at = Instant::now();
let mut latest = None; let mut latest = None;
@@ -391,8 +404,30 @@ fn poll_frame(
let frame = host.last_rendered_frame()?; let frame = host.last_rendered_frame()?;
let paint_ns = elapsed_ns(paint_started_at); let paint_ns = elapsed_ns(paint_started_at);
let encode_started_at = Instant::now(); let encode_started_at = Instant::now();
let has_visible_content = // `non_white`/`content_pixel_count` come from a CPU
frame.non_white_pixel_count() > 0 && frame.content_pixel_count() > 0; // 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.
let has_visible_content = match rendering_context_kind {
RenderingContextKind::Software => {
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 report = LiveFrameReport::new(&snapshot, &frame);
let encode_ns = elapsed_ns(encode_started_at); let encode_ns = elapsed_ns(encode_started_at);
let timings = PartialFrameTimings { paint_ns, encode_ns }; let timings = PartialFrameTimings { paint_ns, encode_ns };