Hold hardware live mode until BGRA presentation
This commit is contained in:
@@ -1,13 +1,12 @@
|
|||||||
//! macOS-only import of cross-process IOSurface handles into
|
//! macOS-only import of cross-process IOSurface handles into
|
||||||
//! `CVPixelBuffer`s suitable for GPUI's `Surface` element.
|
//! `CVPixelBuffer`s that preserve the sidecar's IOSurface identity.
|
||||||
//!
|
//!
|
||||||
//! `T10.4` originally imported the IOSurface into an `MTLTexture`
|
//! `T10.4` originally imported the IOSurface into an `MTLTexture`
|
||||||
//! directly, but GPUI 0.2.2 already speaks `CVPixelBuffer` end-to-end
|
//! directly. GPUI 0.2.2 exposes `Window::paint_surface` /
|
||||||
//! through `Window::paint_surface` / `elements::surface::Surface`. Its
|
//! `elements::surface::Surface` for `CVPixelBuffer`, and that public
|
||||||
//! internal Blade Metal renderer takes care of building the Metal
|
//! path is wired for NV12 video frames. Servo's hardware renderer
|
||||||
//! texture, so a parallel MTLTexture cache here would be wasted work.
|
//! publishes BGRA IOSurfaces, so this cache stays as verified
|
||||||
//! The cache now hands the renderer the CVPixelBuffer GPUI already
|
//! cross-process plumbing until the presenter accepts BGRA surfaces.
|
||||||
//! knows how to render.
|
|
||||||
//!
|
//!
|
||||||
//! Lifetime contract:
|
//! Lifetime contract:
|
||||||
//!
|
//!
|
||||||
@@ -211,7 +210,9 @@ mod tests {
|
|||||||
assert!(mach_port != 0, "IOSurfaceCreateMachPort must yield a real port");
|
assert!(mach_port != 0, "IOSurfaceCreateMachPort must yield a real port");
|
||||||
let surface_id: u64 = 0xDEAD_BEEFu64;
|
let surface_id: u64 = 0xDEAD_BEEFu64;
|
||||||
|
|
||||||
cache.import(mach_port, surface_id).expect("local IOSurface must round-trip into a CVPixelBuffer");
|
cache
|
||||||
|
.import(mach_port, surface_id)
|
||||||
|
.expect("local IOSurface must round-trip into a CVPixelBuffer");
|
||||||
|
|
||||||
let pixel_buffer = cache
|
let pixel_buffer = cache
|
||||||
.pixel_buffer_for(surface_id)
|
.pixel_buffer_for(surface_id)
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ use std::{
|
|||||||
/// (default — bit-identical to pre-flag builds) and `hardware` (real
|
/// (default — bit-identical to pre-flag builds) and `hardware` (real
|
||||||
/// GPU adapter via the vendored `HardwareOffscreenContext`; requires
|
/// GPU adapter via the vendored `HardwareOffscreenContext`; requires
|
||||||
/// the sidecar binary to be compiled with the `hardware-render`
|
/// the sidecar binary to be compiled with the `hardware-render`
|
||||||
/// feature). Anything else is silently dropped and the sidecar
|
/// feature and a GPUI BGRA surface presenter). Anything else is
|
||||||
/// defaults to software so a typo'd value never blocks the browser
|
/// silently dropped and the sidecar defaults to software so a typo'd
|
||||||
/// from starting; the sidecar's own arg parser still errors loudly on
|
/// value never blocks the browser from starting; the sidecar's own
|
||||||
/// an unrecognised value when set explicitly via the flag.
|
/// arg parser still errors loudly on an unrecognised value when set
|
||||||
|
/// explicitly via the flag.
|
||||||
const RENDERING_CONTEXT_ENV: &str = "ELY_SERVO_RENDERING_CONTEXT";
|
const RENDERING_CONTEXT_ENV: &str = "ELY_SERVO_RENDERING_CONTEXT";
|
||||||
|
|
||||||
use ely_domain::SitePermissionDecision;
|
use ely_domain::SitePermissionDecision;
|
||||||
@@ -265,10 +266,10 @@ pub(crate) struct ServoLiveFrame {
|
|||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
sample_hash: u64,
|
sample_hash: u64,
|
||||||
rgba_bytes: Vec<u8>,
|
rgba_bytes: Vec<u8>,
|
||||||
/// Hardware-path companion: when present, the renderer can hand
|
/// Hardware-path companion: the imported IOSurface published by
|
||||||
/// the underlying IOSurface straight to GPUI's Metal pipeline via
|
/// the sidecar. GPUI 0.2.2 presents `surface(...)` through its
|
||||||
/// `gpui::surface(...)` and skip the RGBA upload entirely. Always
|
/// NV12 video path, so the current BGRA Servo surface stays as
|
||||||
/// `None` on the software path and on non-macOS hosts.
|
/// observability plumbing until a BGRA presenter lands.
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pixel_buffer: Option<CVPixelBuffer>,
|
pixel_buffer: Option<CVPixelBuffer>,
|
||||||
}
|
}
|
||||||
@@ -294,8 +295,9 @@ impl ServoLiveFrame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the imported `CVPixelBuffer` matching the frame's
|
/// Returns the imported `CVPixelBuffer` matching the frame's
|
||||||
/// current hardware surface, if any. The renderer hands this to
|
/// current hardware surface, if any. The renderer keeps this as
|
||||||
/// `gpui::surface(...)` to skip the RGBA→texture upload path.
|
/// wire-path evidence while GPUI's public `surface(...)` element
|
||||||
|
/// remains NV12-only.
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn pixel_buffer(&self) -> Option<&CVPixelBuffer> {
|
pub fn pixel_buffer(&self) -> Option<&CVPixelBuffer> {
|
||||||
@@ -412,9 +414,52 @@ pub(crate) enum ServoLiveError {
|
|||||||
/// truth for what values are valid — we just gate which ones we
|
/// truth for what values are valid — we just gate which ones we
|
||||||
/// forward.
|
/// forward.
|
||||||
fn rendering_context_from_env() -> Option<&'static str> {
|
fn rendering_context_from_env() -> Option<&'static str> {
|
||||||
match env::var(RENDERING_CONTEXT_ENV).ok()?.to_lowercase().as_str() {
|
let raw = env::var(RENDERING_CONTEXT_ENV).ok()?;
|
||||||
"software" => Some("software"),
|
match rendering_context_selection(raw.as_str()) {
|
||||||
"hardware" => Some("hardware"),
|
RenderingContextSelection::Forward(value) => Some(value),
|
||||||
_ => None,
|
RenderingContextSelection::HoldHardware => {
|
||||||
|
tracing::warn!(
|
||||||
|
target: "ely::servo::iosurface",
|
||||||
|
"hardware rendering context requested; GPUI 0.2.2 surface presenter accepts NV12 CVPixelBuffers; Servo publishes BGRA IOSurfaces; using software rendering context",
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
RenderingContextSelection::Ignore => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
enum RenderingContextSelection {
|
||||||
|
Forward(&'static str),
|
||||||
|
HoldHardware,
|
||||||
|
Ignore,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rendering_context_selection(raw: &str) -> RenderingContextSelection {
|
||||||
|
match raw.to_lowercase().as_str() {
|
||||||
|
"software" => RenderingContextSelection::Forward("software"),
|
||||||
|
"hardware" => RenderingContextSelection::HoldHardware,
|
||||||
|
_ => RenderingContextSelection::Ignore,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{RenderingContextSelection, rendering_context_selection};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hardware_env_is_held_until_gpui_can_present_bgra_surfaces() {
|
||||||
|
assert_eq!(
|
||||||
|
rendering_context_selection("hardware"),
|
||||||
|
RenderingContextSelection::HoldHardware
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn software_env_still_forwards_to_the_sidecar() {
|
||||||
|
assert_eq!(
|
||||||
|
rendering_context_selection("software"),
|
||||||
|
RenderingContextSelection::Forward("software"),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,15 +57,14 @@ pub(super) struct WebSurfaceFrame {
|
|||||||
content_pixel_count: u64,
|
content_pixel_count: u64,
|
||||||
#[cfg(all(test, feature = "live-site-smoke"))]
|
#[cfg(all(test, feature = "live-site-smoke"))]
|
||||||
sample_hash: u64,
|
sample_hash: u64,
|
||||||
/// Software-path image. `None` whenever the sidecar took the
|
/// Software-path image. Current GPUI builds require this for every
|
||||||
/// hardware shortcut and dropped the RGBA payload from the
|
/// ready web frame because BGRA IOSurface presentation is still
|
||||||
/// wire — the IOSurface in `pixel_buffer` is the source of truth
|
/// held at the protocol boundary.
|
||||||
/// for that frame.
|
|
||||||
pub(super) image: Option<Arc<RenderImage>>,
|
pub(super) image: Option<Arc<RenderImage>>,
|
||||||
/// Hardware-path companion: when present, the view samples the
|
/// Hardware-path companion imported from the sidecar. GPUI 0.2.2's
|
||||||
/// IOSurface through GPUI's Metal pipeline via `gpui::surface(...)`
|
/// public `surface(...)` presenter accepts NV12 video buffers, and
|
||||||
/// instead of uploading the RGBA bytes again. Always `None` on
|
/// Servo publishes BGRA IOSurfaces; this remains observability
|
||||||
/// the software path; `image` is the source of truth there.
|
/// state until a BGRA presenter is available.
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub(super) pixel_buffer: Option<CVPixelBuffer>,
|
pub(super) pixel_buffer: Option<CVPixelBuffer>,
|
||||||
}
|
}
|
||||||
@@ -103,27 +102,23 @@ impl WebSurfaceFrame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn from_parts(parts: WebSurfaceFrameParts) -> Result<Self, WebSurfaceError> {
|
fn from_parts(parts: WebSurfaceFrameParts) -> Result<Self, WebSurfaceError> {
|
||||||
// Hardware path frames arrive with `rgba_bytes` empty — the
|
if parts.rgba_bytes.is_empty() {
|
||||||
// sidecar dropped the 8 MB payload from the wire and the
|
return Err(WebSurfaceError::MissingRenderablePayload);
|
||||||
// receiver samples the IOSurface directly. In that case the
|
}
|
||||||
// RGBA hash + LAST_FRAME_IMAGE dedup are skipped entirely.
|
|
||||||
let image = if parts.rgba_bytes.is_empty() {
|
// Servo's `read_pixels(gl::RGBA, gl::UNSIGNED_BYTE)` writes
|
||||||
None
|
// R-G-B-A in memory order. GPUI's `RenderImage` is documented
|
||||||
} else {
|
// as "in BGRA format" and uploads via
|
||||||
// Servo's `read_pixels(gl::RGBA, gl::UNSIGNED_BYTE)`
|
// `MTLPixelFormat::BGRA8Unorm`, which reads B-G-R-A. Hand the bytes across
|
||||||
// writes R-G-B-A in memory order. GPUI's `RenderImage` is
|
// unchanged and the Metal sampler treats R as B (and vice
|
||||||
// documented as "in BGRA format" and uploads via
|
// versa) — every coloured pixel renders with R and B swapped.
|
||||||
// `MTLPixelFormat::BGRA8Unorm`, which reads B-G-R-A. Hand
|
// Swap once here so the rest of the pipeline (dedup hash,
|
||||||
// the bytes across unchanged and the Metal sampler treats
|
// image buffer, GPU upload) all operate on the same BGRA
|
||||||
// R as B (and vice versa) — every coloured pixel renders
|
// representation.
|
||||||
// with R and B swapped. Swap once here so the rest of the
|
let mut bytes = parts.rgba_bytes;
|
||||||
// pipeline (dedup hash, image buffer, GPU upload) all
|
swap_red_blue_in_place(&mut bytes);
|
||||||
// operate on the same BGRA representation.
|
let bytes_hash = rgba_hash(&bytes);
|
||||||
let mut bytes = parts.rgba_bytes;
|
let image = Some(resolve_render_image(parts.width, parts.height, bytes, bytes_hash)?);
|
||||||
swap_red_blue_in_place(&mut bytes);
|
|
||||||
let bytes_hash = rgba_hash(&bytes);
|
|
||||||
Some(resolve_render_image(parts.width, parts.height, bytes, bytes_hash)?)
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
requested_url: parts.requested_url,
|
requested_url: parts.requested_url,
|
||||||
@@ -245,6 +240,10 @@ struct WebSurfaceFrameParts {
|
|||||||
pub(super) enum WebSurfaceError {
|
pub(super) enum WebSurfaceError {
|
||||||
#[error("invalid servo frame buffer for {width}x{height}")]
|
#[error("invalid servo frame buffer for {width}x{height}")]
|
||||||
InvalidFrameBuffer { width: u32, height: u32 },
|
InvalidFrameBuffer { width: u32, height: u32 },
|
||||||
|
#[error(
|
||||||
|
"servo live frame did not include renderable pixels; BGRA IOSurface presentation is unavailable in GPUI 0.2.2"
|
||||||
|
)]
|
||||||
|
MissingRenderablePayload,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Swap byte 0 and byte 2 of every 4-byte pixel, converting Servo's
|
/// Swap byte 0 and byte 2 of every 4-byte pixel, converting Servo's
|
||||||
@@ -268,20 +267,18 @@ fn resolve_render_image(
|
|||||||
rgba_bytes: Vec<u8>,
|
rgba_bytes: Vec<u8>,
|
||||||
bytes_hash: u64,
|
bytes_hash: u64,
|
||||||
) -> Result<Arc<RenderImage>, WebSurfaceError> {
|
) -> Result<Arc<RenderImage>, WebSurfaceError> {
|
||||||
LAST_FRAME_IMAGE.with(
|
LAST_FRAME_IMAGE.with(|cache| -> Result<Arc<RenderImage>, WebSurfaceError> {
|
||||||
|cache| -> Result<Arc<RenderImage>, WebSurfaceError> {
|
let mut cache = cache.borrow_mut();
|
||||||
let mut cache = cache.borrow_mut();
|
if let Some((cached_hash, cached_image)) = cache.as_ref() {
|
||||||
if let Some((cached_hash, cached_image)) = cache.as_ref() {
|
if *cached_hash == bytes_hash {
|
||||||
if *cached_hash == bytes_hash {
|
return Ok(cached_image.clone());
|
||||||
return Ok(cached_image.clone());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let image_buffer = ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, rgba_bytes)
|
let image_buffer = ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, rgba_bytes)
|
||||||
.ok_or(WebSurfaceError::InvalidFrameBuffer { width, height })?;
|
.ok_or(WebSurfaceError::InvalidFrameBuffer { width, height })?;
|
||||||
let new_image = Arc::new(RenderImage::new([image::Frame::new(image_buffer)]));
|
let new_image = Arc::new(RenderImage::new([image::Frame::new(image_buffer)]));
|
||||||
*cache = Some((bytes_hash, new_image.clone()));
|
*cache = Some((bytes_hash, new_image.clone()));
|
||||||
Ok(new_image)
|
Ok(new_image)
|
||||||
},
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -383,6 +383,28 @@ fn live_frame_swaps_red_and_blue_bytes_for_gpui_bgra() -> Result<(), Box<dyn Err
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_live_frame_payload_is_rejected() {
|
||||||
|
use crate::services::servo_live::ServoLiveFrame;
|
||||||
|
use crate::shell::web_surface_frame::WebSurfaceFrame;
|
||||||
|
use crate::shell::web_surface_geometry::WebSurfaceScrollOffset;
|
||||||
|
|
||||||
|
let result = WebSurfaceFrame::from_live_frame(
|
||||||
|
"https://example.com/".to_string(),
|
||||||
|
WebSurfaceScrollOffset::default(),
|
||||||
|
100,
|
||||||
|
ServoLiveFrame::for_test(1, 1, Vec::new()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let Err(error) = result else {
|
||||||
|
panic!("empty Servo frame payload must be rejected before it reaches Ready state");
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
error.to_string(),
|
||||||
|
"servo live frame did not include renderable pixels; BGRA IOSurface presentation is unavailable in GPUI 0.2.2",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn web_bounds() -> Bounds<gpui::Pixels> {
|
fn web_bounds() -> Bounds<gpui::Pixels> {
|
||||||
Bounds::new(point(px(0.0), px(0.0)), size(px(640.0), px(480.0)))
|
Bounds::new(point(px(0.0), px(0.0)), size(px(640.0), px(480.0)))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ pub(super) fn render_ready_web_surface(
|
|||||||
tab: &BrowserTab,
|
tab: &BrowserTab,
|
||||||
state_entity: Entity<ElyShell>,
|
state_entity: Entity<ElyShell>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
// T14: the `gpui::surface(...)` hardware path is disabled.
|
// T14: the `gpui::surface(...)` hardware path is held.
|
||||||
//
|
//
|
||||||
// GPUI 0.2.2's Blade Metal renderer hard-asserts that any
|
// GPUI 0.2.2's Blade Metal renderer hard-asserts that any
|
||||||
// CVPixelBuffer handed to `surface(...)` is NV12 YUV
|
// CVPixelBuffer handed to `surface(...)` is NV12 YUV
|
||||||
@@ -35,10 +35,11 @@ pub(super) fn render_ready_web_surface(
|
|||||||
img(ImageSource::Render(image.clone())).size_full().object_fit(ObjectFit::Fill),
|
img(ImageSource::Render(image.clone())).size_full().object_fit(ObjectFit::Fill),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Both image variants empty: the sidecar should always publish
|
render_web_surface(
|
||||||
// RGBA while the hardware path is disabled, but a blank canvas is
|
tab,
|
||||||
// the honest user-facing fallback if it ever does not.
|
state_entity,
|
||||||
render_web_surface(tab, state_entity, div().size_full())
|
error_page("Web surface frame did not include renderable pixels."),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn render_loading_web_surface(
|
pub(super) fn render_loading_web_surface(
|
||||||
|
|||||||
Reference in New Issue
Block a user