Add Servo site compatibility frame smoke
This commit is contained in:
@@ -20,4 +20,7 @@ pub enum ServoHostError {
|
|||||||
|
|
||||||
#[error("servo rendering context could not be made current")]
|
#[error("servo rendering context could not be made current")]
|
||||||
RenderingContextNotCurrent,
|
RenderingContextNotCurrent,
|
||||||
|
|
||||||
|
#[error("servo rendered frame is unavailable")]
|
||||||
|
RenderedFrameUnavailable,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,118 @@ pub enum WebViewState {
|
|||||||
Crashed,
|
Crashed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct RenderedFrameSummary {
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
opaque_pixel_count: u64,
|
||||||
|
non_white_pixel_count: u64,
|
||||||
|
sample_hash: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderedFrameSummary {
|
||||||
|
#[must_use]
|
||||||
|
pub fn from_rgba_bytes(width: u32, height: u32, rgba_bytes: &[u8]) -> Self {
|
||||||
|
let mut opaque_pixel_count = 0;
|
||||||
|
let mut non_white_pixel_count = 0;
|
||||||
|
let mut sample_hash = 0xcbf29ce484222325_u64;
|
||||||
|
|
||||||
|
for (index, pixel) in rgba_bytes.chunks_exact(4).enumerate() {
|
||||||
|
let [red, green, blue, alpha] = [pixel[0], pixel[1], pixel[2], pixel[3]];
|
||||||
|
if alpha > 0 {
|
||||||
|
opaque_pixel_count += 1;
|
||||||
|
}
|
||||||
|
if alpha > 0 && (red < 245 || green < 245 || blue < 245) {
|
||||||
|
non_white_pixel_count += 1;
|
||||||
|
}
|
||||||
|
if index % 97 == 0 {
|
||||||
|
for byte in pixel {
|
||||||
|
sample_hash ^= u64::from(*byte);
|
||||||
|
sample_hash = sample_hash.wrapping_mul(0x100000001b3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self { width, height, opaque_pixel_count, non_white_pixel_count, sample_hash }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn width(&self) -> u32 {
|
||||||
|
self.width
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn height(&self) -> u32 {
|
||||||
|
self.height
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn opaque_pixel_count(&self) -> u64 {
|
||||||
|
self.opaque_pixel_count
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn non_white_pixel_count(&self) -> u64 {
|
||||||
|
self.non_white_pixel_count
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn sample_hash(&self) -> u64 {
|
||||||
|
self.sample_hash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct RenderedFrame {
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
rgba_bytes: Vec<u8>,
|
||||||
|
summary: RenderedFrameSummary,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderedFrame {
|
||||||
|
#[must_use]
|
||||||
|
pub fn from_rgba_bytes(width: u32, height: u32, rgba_bytes: Vec<u8>) -> Self {
|
||||||
|
let summary = RenderedFrameSummary::from_rgba_bytes(width, height, &rgba_bytes);
|
||||||
|
Self { width, height, rgba_bytes, summary }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn width(&self) -> u32 {
|
||||||
|
self.width
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn height(&self) -> u32 {
|
||||||
|
self.height
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn rgba_bytes(&self) -> &[u8] {
|
||||||
|
&self.rgba_bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn summary(&self) -> &RenderedFrameSummary {
|
||||||
|
&self.summary
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn opaque_pixel_count(&self) -> u64 {
|
||||||
|
self.summary.opaque_pixel_count()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn non_white_pixel_count(&self) -> u64 {
|
||||||
|
self.summary.non_white_pixel_count()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn sample_hash(&self) -> u64 {
|
||||||
|
self.summary.sample_hash()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct WebViewSnapshot {
|
pub struct WebViewSnapshot {
|
||||||
webview_id: WebViewId,
|
webview_id: WebViewId,
|
||||||
@@ -116,4 +228,6 @@ pub trait ServoHost {
|
|||||||
fn tick(&mut self) -> bool;
|
fn tick(&mut self) -> bool;
|
||||||
|
|
||||||
fn paint(&mut self, webview_id: &WebViewId) -> Result<(), ServoHostError>;
|
fn paint(&mut self, webview_id: &WebViewId) -> Result<(), ServoHostError>;
|
||||||
|
|
||||||
|
fn last_rendered_frame(&self) -> Result<RenderedFrame, ServoHostError>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ mod runtime;
|
|||||||
|
|
||||||
pub use error::ServoHostError;
|
pub use error::ServoHostError;
|
||||||
pub use host::{
|
pub use host::{
|
||||||
NavigationRequest, PermissionDecision, PermissionRequest, ServoHost, WebViewSnapshot,
|
NavigationRequest, PermissionDecision, PermissionRequest, RenderedFrame, RenderedFrameSummary,
|
||||||
WebViewState,
|
ServoHost, WebViewSnapshot, WebViewState,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "servo-engine")]
|
#[cfg(feature = "servo-engine")]
|
||||||
pub use runtime::{ServoSurfaceSize, SoftwareServoHost};
|
pub use runtime::{ServoSurfaceSize, SoftwareServoHost};
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ use std::{
|
|||||||
use dpi::PhysicalSize;
|
use dpi::PhysicalSize;
|
||||||
use ely_domain::{ProfileId, TabId, WebViewId};
|
use ely_domain::{ProfileId, TabId, WebViewId};
|
||||||
use servo::{
|
use servo::{
|
||||||
EventLoopWaker, LoadStatus, RenderingContext, Servo, ServoBuilder, WebView, WebViewBuilder,
|
DeviceIntPoint, DeviceIntRect, DeviceIntSize, EventLoopWaker, LoadStatus, RenderingContext,
|
||||||
WebViewDelegate,
|
Servo, ServoBuilder, WebView, WebViewBuilder, WebViewDelegate,
|
||||||
};
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
NavigationRequest, PermissionDecision, PermissionRequest, ServoHost, ServoHostError,
|
NavigationRequest, PermissionDecision, PermissionRequest, RenderedFrame, ServoHost,
|
||||||
WebViewSnapshot, WebViewState,
|
ServoHostError, WebViewSnapshot, WebViewState,
|
||||||
};
|
};
|
||||||
|
|
||||||
static SERVO_RUNTIME_STARTED: AtomicBool = AtomicBool::new(false);
|
static SERVO_RUNTIME_STARTED: AtomicBool = AtomicBool::new(false);
|
||||||
@@ -46,6 +46,7 @@ pub struct SoftwareServoHost {
|
|||||||
webviews: HashMap<WebViewId, HostWebView>,
|
webviews: HashMap<WebViewId, HostWebView>,
|
||||||
permissions: PermissionStore,
|
permissions: PermissionStore,
|
||||||
wake_requested: Arc<AtomicBool>,
|
wake_requested: Arc<AtomicBool>,
|
||||||
|
last_rendered_frame: Option<RenderedFrame>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SoftwareServoHost {
|
impl SoftwareServoHost {
|
||||||
@@ -82,6 +83,7 @@ impl SoftwareServoHost {
|
|||||||
webviews: HashMap::new(),
|
webviews: HashMap::new(),
|
||||||
permissions: Rc::new(RefCell::new(HashMap::new())),
|
permissions: Rc::new(RefCell::new(HashMap::new())),
|
||||||
wake_requested,
|
wake_requested,
|
||||||
|
last_rendered_frame: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,16 +173,24 @@ impl ServoHost for SoftwareServoHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn paint(&mut self, webview_id: &WebViewId) -> Result<(), ServoHostError> {
|
fn paint(&mut self, webview_id: &WebViewId) -> Result<(), ServoHostError> {
|
||||||
let webview = self.webview(webview_id)?;
|
|
||||||
self.rendering_context
|
self.rendering_context
|
||||||
.make_current()
|
.make_current()
|
||||||
.map_err(|_| ServoHostError::RenderingContextNotCurrent)?;
|
.map_err(|_| ServoHostError::RenderingContextNotCurrent)?;
|
||||||
self.rendering_context.prepare_for_rendering();
|
self.rendering_context.prepare_for_rendering();
|
||||||
|
{
|
||||||
|
let webview = self.webview(webview_id)?;
|
||||||
webview.webview.paint();
|
webview.webview.paint();
|
||||||
|
}
|
||||||
|
let rendered_frame = self.read_rendered_frame()?;
|
||||||
self.rendering_context.present();
|
self.rendering_context.present();
|
||||||
webview.delegate.mark_frame_presented();
|
self.webview(webview_id)?.delegate.mark_frame_presented();
|
||||||
|
self.last_rendered_frame = Some(rendered_frame);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn last_rendered_frame(&self) -> Result<RenderedFrame, ServoHostError> {
|
||||||
|
self.last_rendered_frame.clone().ok_or(ServoHostError::RenderedFrameUnavailable)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SoftwareServoHost {
|
impl SoftwareServoHost {
|
||||||
@@ -189,6 +199,24 @@ impl SoftwareServoHost {
|
|||||||
.get(webview_id)
|
.get(webview_id)
|
||||||
.ok_or_else(|| ServoHostError::WebViewNotFound { id: webview_id.clone() })
|
.ok_or_else(|| ServoHostError::WebViewNotFound { id: webview_id.clone() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_rendered_frame(&self) -> Result<RenderedFrame, ServoHostError> {
|
||||||
|
let size = self.rendering_context.size();
|
||||||
|
let width =
|
||||||
|
i32::try_from(size.width).map_err(|_| ServoHostError::RenderedFrameUnavailable)?;
|
||||||
|
let height =
|
||||||
|
i32::try_from(size.height).map_err(|_| ServoHostError::RenderedFrameUnavailable)?;
|
||||||
|
let frame_rect = DeviceIntRect::from_origin_and_size(
|
||||||
|
DeviceIntPoint::new(0, 0),
|
||||||
|
DeviceIntSize::new(width, height),
|
||||||
|
);
|
||||||
|
let image = self
|
||||||
|
.rendering_context
|
||||||
|
.read_to_image(frame_rect)
|
||||||
|
.ok_or(ServoHostError::RenderedFrameUnavailable)?;
|
||||||
|
|
||||||
|
Ok(RenderedFrame::from_rgba_bytes(size.width, size.height, image.into_raw()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HostWebView {
|
struct HostWebView {
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ use ely_servo_host::{
|
|||||||
NavigationRequest, ServoHost, ServoHostError, ServoSurfaceSize, SoftwareServoHost, WebViewState,
|
NavigationRequest, ServoHost, ServoHostError, ServoSurfaceSize, SoftwareServoHost, WebViewState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PRD_SITE_COMPATIBILITY_URLS: &[&str] = &["https://example.com", "https://servo.org"];
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn manages_real_servo_webview_lifecycle() -> Result<(), Box<dyn Error>> {
|
fn manages_real_servo_webview_lifecycle() -> Result<(), Box<dyn Error>> {
|
||||||
let mut host = SoftwareServoHost::new(ServoSurfaceSize::new(320, 240))?;
|
let mut host = SoftwareServoHost::new(ServoSurfaceSize::new(640, 480))?;
|
||||||
let tab_id = TabId::new();
|
let tab_id = TabId::new();
|
||||||
let profile_id = ProfileId::new();
|
let profile_id = ProfileId::new();
|
||||||
|
|
||||||
@@ -27,24 +29,80 @@ fn manages_real_servo_webview_lifecycle() -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
host.navigate(NavigationRequest { webview_id: webview_id.clone(), tab_id, url })?;
|
host.navigate(NavigationRequest { webview_id: webview_id.clone(), tab_id, url })?;
|
||||||
|
|
||||||
for _ in 0..1_000 {
|
let snapshot = wait_for_rendered_webview(&mut host, &webview_id, None)?;
|
||||||
host.tick();
|
|
||||||
if host.state(&webview_id)? == WebViewState::Complete {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
thread::sleep(Duration::from_millis(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
let snapshot = host.snapshot(&webview_id)?;
|
|
||||||
assert_eq!(snapshot.state(), &WebViewState::Complete, "snapshot: {snapshot:?}");
|
assert_eq!(snapshot.state(), &WebViewState::Complete, "snapshot: {snapshot:?}");
|
||||||
assert!(
|
assert!(
|
||||||
snapshot.url().is_some_and(|value| value.starts_with("data:text/html,")),
|
snapshot.url().is_some_and(|value| value.starts_with("data:text/html,")),
|
||||||
"snapshot: {snapshot:?}"
|
"snapshot: {snapshot:?}"
|
||||||
);
|
);
|
||||||
|
assert_rendered_frame_has_content(&host, "data:text/html")?;
|
||||||
|
|
||||||
|
let mut previous_frame_hash = Some(host.last_rendered_frame()?.sample_hash());
|
||||||
|
for site_url in PRD_SITE_COMPATIBILITY_URLS {
|
||||||
|
let tab_id = TabId::new();
|
||||||
|
let url = UrlText::parse(*site_url)?;
|
||||||
|
|
||||||
|
host.navigate(NavigationRequest { webview_id: webview_id.clone(), tab_id, url })?;
|
||||||
|
let snapshot = wait_for_rendered_webview(&mut host, &webview_id, previous_frame_hash)?;
|
||||||
|
|
||||||
|
assert_eq!(snapshot.state(), &WebViewState::Complete, "{site_url}: {snapshot:?}");
|
||||||
|
assert!(
|
||||||
|
snapshot.url().is_some_and(|value| value.starts_with(site_url)),
|
||||||
|
"{site_url}: {snapshot:?}"
|
||||||
|
);
|
||||||
|
assert_rendered_frame_has_content(&host, site_url)?;
|
||||||
|
previous_frame_hash = Some(host.last_rendered_frame()?.sample_hash());
|
||||||
|
}
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
SoftwareServoHost::new(ServoSurfaceSize::new(320, 240)),
|
SoftwareServoHost::new(ServoSurfaceSize::new(640, 480)),
|
||||||
Err(ServoHostError::RuntimeAlreadyStarted)
|
Err(ServoHostError::RuntimeAlreadyStarted)
|
||||||
));
|
));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn wait_for_rendered_webview(
|
||||||
|
host: &mut SoftwareServoHost,
|
||||||
|
webview_id: &ely_domain::WebViewId,
|
||||||
|
previous_frame_hash: Option<u64>,
|
||||||
|
) -> Result<ely_servo_host::WebViewSnapshot, Box<dyn Error>> {
|
||||||
|
let mut painted_since_request = false;
|
||||||
|
|
||||||
|
for _ in 0..5_000 {
|
||||||
|
host.tick();
|
||||||
|
let snapshot = host.snapshot(webview_id)?;
|
||||||
|
if snapshot.has_pending_frame() {
|
||||||
|
host.paint(webview_id)?;
|
||||||
|
painted_since_request = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let snapshot = host.snapshot(webview_id)?;
|
||||||
|
let has_rendered_current_request = host.last_rendered_frame().is_ok_and(|frame| {
|
||||||
|
painted_since_request
|
||||||
|
&& Some(frame.sample_hash()) != previous_frame_hash
|
||||||
|
&& frame.non_white_pixel_count() > 0
|
||||||
|
});
|
||||||
|
|
||||||
|
if snapshot.state() == &WebViewState::Complete && has_rendered_current_request {
|
||||||
|
return Ok(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_millis(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(format!("timed out waiting for rendered webview: {:?}", host.snapshot(webview_id)?).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_rendered_frame_has_content(
|
||||||
|
host: &SoftwareServoHost,
|
||||||
|
label: &str,
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
let frame = host.last_rendered_frame()?;
|
||||||
|
|
||||||
|
assert_eq!(frame.width(), 640, "{label}: {frame:?}");
|
||||||
|
assert_eq!(frame.height(), 480, "{label}: {frame:?}");
|
||||||
|
assert!(frame.opaque_pixel_count() > 0, "{label}: {frame:?}");
|
||||||
|
assert!(frame.non_white_pixel_count() > 0, "{label}: {frame:?}");
|
||||||
|
assert_ne!(frame.sample_hash(), 0, "{label}: {frame:?}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user