diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /gfx/wr/wrench/src | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'gfx/wr/wrench/src')
-rw-r--r-- | gfx/wr/wrench/src/angle.rs | 62 | ||||
-rw-r--r-- | gfx/wr/wrench/src/args.yaml | 188 | ||||
-rw-r--r-- | gfx/wr/wrench/src/blob.rs | 213 | ||||
-rw-r--r-- | gfx/wr/wrench/src/egl.rs | 611 | ||||
-rw-r--r-- | gfx/wr/wrench/src/main.rs | 1040 | ||||
-rw-r--r-- | gfx/wr/wrench/src/parse_function.rs | 134 | ||||
-rw-r--r-- | gfx/wr/wrench/src/perf.rs | 349 | ||||
-rw-r--r-- | gfx/wr/wrench/src/png.rs | 118 | ||||
-rw-r--r-- | gfx/wr/wrench/src/premultiply.rs | 56 | ||||
-rw-r--r-- | gfx/wr/wrench/src/rawtest.rs | 1450 | ||||
-rw-r--r-- | gfx/wr/wrench/src/reftest.rs | 970 | ||||
-rw-r--r-- | gfx/wr/wrench/src/test_invalidation.rs | 129 | ||||
-rw-r--r-- | gfx/wr/wrench/src/test_shaders.rs | 161 | ||||
-rw-r--r-- | gfx/wr/wrench/src/wrench.rs | 641 | ||||
-rw-r--r-- | gfx/wr/wrench/src/yaml_frame_reader.rs | 2125 | ||||
-rw-r--r-- | gfx/wr/wrench/src/yaml_helper.rs | 923 |
16 files changed, 9170 insertions, 0 deletions
diff --git a/gfx/wr/wrench/src/angle.rs b/gfx/wr/wrench/src/angle.rs new file mode 100644 index 0000000000..94be95a625 --- /dev/null +++ b/gfx/wr/wrench/src/angle.rs @@ -0,0 +1,62 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use glutin::{self, ContextBuilder, ContextCurrentState, CreationError}; +use winit::{event_loop::EventLoop, window::Window, window::WindowBuilder}; + +#[cfg(not(windows))] +pub enum Context {} + +#[cfg(windows)] +pub use crate::egl::Context; + +impl Context { + #[cfg(not(windows))] + pub fn with_window<T: ContextCurrentState>( + _: WindowBuilder, + _: ContextBuilder<'_, T>, + _: &EventLoop<()>, + ) -> Result<(Window, Self), CreationError> { + Err(CreationError::PlatformSpecific( + "ANGLE rendering is only supported on Windows".into(), + )) + } + + #[cfg(windows)] + pub fn with_window<T: ContextCurrentState>( + window_builder: WindowBuilder, + context_builder: ContextBuilder<'_, T>, + events_loop: &EventLoop<()>, + ) -> Result<(Window, Self), CreationError> { + use winit::platform::windows::WindowExtWindows; + + // FIXME: &context_builder.pf_reqs https://github.com/tomaka/glutin/pull/1002 + let pf_reqs = &glutin::PixelFormatRequirements::default(); + let gl_attr = &context_builder.gl_attr.map_sharing(|_| unimplemented!()); + let window = window_builder.build(events_loop)?; + Self::new(pf_reqs, gl_attr) + .and_then(|p| p.finish(window.hwnd() as _)) + .map(|context| (window, context)) + } + + #[cfg(not(windows))] + pub unsafe fn make_current(&self) -> Result<(), glutin::ContextError> { + match *self {} + } + + #[cfg(not(windows))] + pub fn get_proc_address(&self, _: &str) -> *const () { + match *self {} + } + + #[cfg(not(windows))] + pub fn swap_buffers(&self) -> Result<(), glutin::ContextError> { + match *self {} + } + + #[cfg(not(windows))] + pub fn get_api(&self) -> glutin::Api { + match *self {} + } +} diff --git a/gfx/wr/wrench/src/args.yaml b/gfx/wr/wrench/src/args.yaml new file mode 100644 index 0000000000..f55a419571 --- /dev/null +++ b/gfx/wr/wrench/src/args.yaml @@ -0,0 +1,188 @@ +name: wrench +version: "0.1" +author: Vladimir Vukicevic <vladimir@pobox.com> +about: WebRender testing and debugging utility + +args: + - precache: + short: c + long: precache + help: Precache shaders + - verbose: + short: v + long: verbose + help: Enable verbose display + - shaders: + long: shaders + help: Override path for shaders + takes_value: true + - use_unoptimized_shaders: + long: use-unoptimized-shaders + help: Use unoptimized shaders rather than the shaders optimized at build-time + - rebuild: + short: r + long: rebuild + help: Rebuild display list from scratch every frame + - no_subpixel_aa: + short: a + long: no-subpixel-aa + help: Disable subpixel aa + - slow_subpixel: + long: slow-subpixel + help: Disable dual source blending + - headless: + long: headless + help: Enable headless rendering + - angle: + long: angle + help: Enable ANGLE rendering (on Windows only) + - software: + long: software + help: Enable software rendering + - size: + short: s + long: size + help: Window size, specified as widthxheight (e.g. 1024x768), in pixels + takes_value: true + - vsync: + long: vsync + help: Enable vsync for OpenGL window + - no_scissor: + long: no-scissor + help: Disable scissors when clearing render targets + - no_batch: + long: no-batch + help: Disable batching of instanced draw calls + - chase: + long: chase + help: Chase a particular primitive matching the local rect or ID + takes_value: true + - dump_shader_source: + long: dump-shader-source + help: Dump the source of the specified shader + takes_value: true + global: true + - renderer: + long: renderer + help: Select rendering API (gl3, es3) + takes_value: true + global: true + - no_block: + long: no-block + help: Don't block on UI events - run event loop as fast as possible. + - profiler_ui: + long: profiler-ui + takes_value: true + help: A string describing what to show on in the profiler HUD (See https://github.com/servo/webrender/wiki/Debugging-WebRender#anchor_6). + +subcommands: + - png: + about: render frame described by YAML and save it to a png file + args: + - surface: + short: s + long: surface + help: 'What rendered surface to save as PNG, one of: screen, gpu-cache' + takes_value: true + - INPUT: + help: The input YAML file + required: true + index: 1 + - OUTPUT: + help: Optional output path to save to. + required: false + index: 2 + - show: + about: show frame(s) described by YAML, binary recording, or capture + aliases: ['load', 'replay'] + args: + - include: + long: include + help: Include the given element type. Can be specified multiple times. (rect/image/text/glyphs/border) (YAML only) + multiple: true + takes_value: true + - list-resources: + long: list-resources + help: List the resources used by this render (YAML only) + - watch: + short: w + long: watch + help: Watch the given file, reloading whenever it changes (YAML only) + - keyframes: + short: k + long: keyframes + takes_value: true + help: Provide a keyframes file, that can be used to animate the yaml input file + - scene-id: + short: s + long: scene-id + takes_value: true + help: Select a starting scene sequence ID (YAML capture sequence only). + - frame-id: + short: f + long: frame-id + takes_value: true + help: Select a starting frame sequence ID (YAML capture sequence only). + - INPUT: + help: The input YAML, binary recording, or capture directory + required: true + index: 1 + - reftest: + about: run reftests + args: + - fuzz_tolerance: + long: fuzzy + takes_value: true + help: Add a minimum fuzziness tolerance to all tests. + required: false + - REFTEST: + help: a specific reftest or directory to run + required: false + index: 1 + - rawtest: + about: run rawtests + - perf: + about: run benchmarks + args: + - filename: + help: name of the file to save benchmarks to + required: true + index: 1 + - benchmark: + help: benchmark list filename (default is benchmarks/benchmarks.list) + required: false + index: 2 + - auto-filename: + long: auto-filename + help: generate output filename from date and time (user provided filename is the directory prefix) + required: false + - csv: + long: csv + help: save benchmark results as .csv (default is json) + required: false + - warmup_frames: + long: warmup_frames + takes_value: true + help: number of frames to skip before recording timings + required: false + - sample_count: + long: sample_count + takes_value: true + help: number of samples to capture + - test_invalidation: + about: run invalidation tests + - compare_perf: + about: compare two benchmark files + args: + - first_filename: + help: first benchmark file to compare + required: true + index: 1 + - second_filename: + help: second benchmark file to compare + required: true + index: 2 + - test_init: + about: Test for successful initialization then exit immediately + - test_shaders: + about: run shader tests diff --git a/gfx/wr/wrench/src/blob.rs b/gfx/wr/wrench/src/blob.rs new file mode 100644 index 0000000000..ba291a5b23 --- /dev/null +++ b/gfx/wr/wrench/src/blob.rs @@ -0,0 +1,213 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// A very basic BlobImageRasterizer that can only render a checkerboard pattern. + +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::Mutex; +use webrender::api::*; +use webrender::api::units::{BlobDirtyRect, BlobToDeviceTranslation, TileOffset}; +use webrender::api::units::DeviceIntRect; + +// Serialize/deserialize the blob. + +pub fn serialize_blob(color: ColorU) -> Arc<Vec<u8>> { + Arc::new(vec![color.r, color.g, color.b, color.a]) +} + +fn deserialize_blob(blob: &[u8]) -> Result<ColorU, ()> { + let mut iter = blob.iter(); + match (iter.next(), iter.next(), iter.next(), iter.next()) { + (Some(&r), Some(&g), Some(&b), Some(&a)) => Ok(ColorU::new(r, g, b, a)), + (Some(&a), None, None, None) => Ok(ColorU::new(a, a, a, a)), + _ => Err(()), + } +} + +// perform floor((x * a) / 255. + 0.5) see "Three wrongs make a right" for derivation +fn premul(x: u8, a: u8) -> u8 { + let t = (x as u32) * (a as u32) + 128; + ((t + (t >> 8)) >> 8) as u8 +} + +// This is the function that applies the deserialized drawing commands and generates +// actual image data. +fn render_blob( + color: ColorU, + descriptor: &BlobImageDescriptor, + tile: TileOffset, + _tile_size: TileSize, + dirty_rect: &BlobDirtyRect, +) -> BlobImageResult { + // Allocate storage for the result. Right now the resource cache expects the + // tiles to have have no stride or offset. + let buf_size = descriptor.rect.area() * + descriptor.format.bytes_per_pixel(); + let mut texels = vec![0u8; (buf_size) as usize]; + + // Generate a per-tile pattern to see it in the demo. For a real use case it would not + // make sense for the rendered content to depend on its tile. + let tile_checker = (tile.x % 2 == 0) != (tile.y % 2 == 0); + + let dirty_rect = dirty_rect.to_subrect_of(&descriptor.rect); + + // We want the dirty rect local to the tile rather than the whole image. + let tx: BlobToDeviceTranslation = (-descriptor.rect.min.to_vector()).into(); + + let rasterized_rect = tx.transform_box(&dirty_rect); + + for y in rasterized_rect.min.y .. rasterized_rect.max.y { + for x in rasterized_rect.min.x .. rasterized_rect.max.x { + // Apply the tile's offset. This is important: all drawing commands should be + // translated by this offset to give correct results with tiled blob images. + let x2 = x + descriptor.rect.min.x; + let y2 = y + descriptor.rect.min.y; + + // Render a simple checkerboard pattern + let checker = if (x2 % 20 >= 10) != (y2 % 20 >= 10) { + 1 + } else { + 0 + }; + // ..nested in the per-tile checkerboard pattern + let tc = if tile_checker { 0 } else { (1 - checker) * 40 }; + + match descriptor.format { + ImageFormat::BGRA8 => { + let a = color.a * checker + tc; + let pixel_offset = ((y * descriptor.rect.width() + x) * 4) as usize; + texels[pixel_offset ] = premul(color.b * checker + tc, a); + texels[pixel_offset + 1] = premul(color.g * checker + tc, a); + texels[pixel_offset + 2] = premul(color.r * checker + tc, a); + texels[pixel_offset + 3] = a; + } + ImageFormat::R8 => { + texels[(y * descriptor.rect.width() + x) as usize] = color.a * checker + tc; + } + _ => { + return Err(BlobImageError::Other( + format!("Unsupported image format {:?}", descriptor.format), + )); + } + } + } + } + + Ok(RasterizedBlobImage { + data: Arc::new(texels), + rasterized_rect, + }) +} + +/// See rawtest.rs. We use this to test that blob images are requested the right +/// amount of times. +pub struct BlobCallbacks { + pub request: Box<dyn Fn(&[BlobImageParams]) + Send + 'static>, +} + +impl BlobCallbacks { + pub fn new() -> Self { + BlobCallbacks { request: Box::new(|_|()) } + } +} + +pub struct CheckerboardRenderer { + image_cmds: HashMap<BlobImageKey, (ColorU, TileSize)>, + callbacks: Arc<Mutex<BlobCallbacks>>, +} + +impl CheckerboardRenderer { + pub fn new(callbacks: Arc<Mutex<BlobCallbacks>>) -> Self { + CheckerboardRenderer { + callbacks, + image_cmds: HashMap::new(), + } + } +} + +impl BlobImageHandler for CheckerboardRenderer { + fn create_similar(&self) -> Box<dyn BlobImageHandler> { + Box::new(CheckerboardRenderer::new(Arc::clone(&self.callbacks))) + } + + fn add(&mut self, key: BlobImageKey, cmds: Arc<BlobImageData>, + _visible_rect: &DeviceIntRect, tile_size: TileSize) { + self.image_cmds + .insert(key, (deserialize_blob(&cmds[..]).unwrap(), tile_size)); + } + + fn update(&mut self, key: BlobImageKey, cmds: Arc<BlobImageData>, + _visible_rect: &DeviceIntRect, _dirty_rect: &BlobDirtyRect) { + // Here, updating is just replacing the current version of the commands with + // the new one (no incremental updates). + self.image_cmds.get_mut(&key).unwrap().0 = deserialize_blob(&cmds[..]).unwrap(); + } + + fn delete(&mut self, key: BlobImageKey) { + self.image_cmds.remove(&key); + } + + fn delete_font(&mut self, _key: FontKey) {} + + fn delete_font_instance(&mut self, _key: FontInstanceKey) {} + + fn clear_namespace(&mut self, _namespace: IdNamespace) {} + + fn prepare_resources( + &mut self, + _services: &dyn BlobImageResources, + requests: &[BlobImageParams], + ) { + if !requests.is_empty() { + (self.callbacks.lock().unwrap().request)(requests); + } + } + + fn create_blob_rasterizer(&mut self) -> Box<dyn AsyncBlobImageRasterizer> { + Box::new(Rasterizer { image_cmds: self.image_cmds.clone() }) + } + + fn enable_multithreading(&mut self, _enable: bool) {} +} + +struct Command { + request: BlobImageRequest, + color: ColorU, + descriptor: BlobImageDescriptor, + tile: TileOffset, + tile_size: TileSize, + dirty_rect: BlobDirtyRect, +} + +struct Rasterizer { + image_cmds: HashMap<BlobImageKey, (ColorU, TileSize)>, +} + +impl AsyncBlobImageRasterizer for Rasterizer { + fn rasterize( + &mut self, + requests: &[BlobImageParams], + _low_priority: bool + ) -> Vec<(BlobImageRequest, BlobImageResult)> { + let requests: Vec<Command> = requests.iter().map( + |item| { + let (color, tile_size) = self.image_cmds[&item.request.key]; + + Command { + request: item.request, + color, + tile_size, + tile: item.request.tile, + descriptor: item.descriptor, + dirty_rect: item.dirty_rect, + } + } + ).collect(); + + requests.iter().map(|cmd| { + (cmd.request, render_blob(cmd.color, &cmd.descriptor, cmd.tile, cmd.tile_size, &cmd.dirty_rect)) + }).collect() + } +} diff --git a/gfx/wr/wrench/src/egl.rs b/gfx/wr/wrench/src/egl.rs new file mode 100644 index 0000000000..4b91d8cd86 --- /dev/null +++ b/gfx/wr/wrench/src/egl.rs @@ -0,0 +1,611 @@ +// Licensed under the Apache License, Version 2.0. +// This file may not be copied, modified, or distributed except according to those terms. + +//! Based on https://github.com/tomaka/glutin/blob/1b2d62c0e9/src/api/egl/mod.rs +#![cfg(windows)] +#![allow(unused_variables)] + +use glutin::ContextError; +use glutin::CreationError; +use glutin::GlAttributes; +use glutin::GlRequest; +use glutin::PixelFormat; +use glutin::PixelFormatRequirements; +use glutin::ReleaseBehavior; +use glutin::Robustness; +use glutin::Api; + +use std::ffi::{CStr, CString}; +use std::os::raw::c_int; +use std::ptr; +use std::cell::Cell; + +use mozangle::egl::ffi as egl; +mod ffi { + pub use mozangle::egl::ffi as egl; + pub use mozangle::egl::ffi::*; +} + +pub struct Context { + display: ffi::egl::types::EGLDisplay, + context: ffi::egl::types::EGLContext, + surface: Cell<ffi::egl::types::EGLSurface>, + api: Api, + pixel_format: PixelFormat, +} + +impl Context { + /// Start building an EGL context. + /// + /// This function initializes some things and chooses the pixel format. + /// + /// To finish the process, you must call `.finish(window)` on the `ContextPrototype`. + pub fn new<'a>( + pf_reqs: &PixelFormatRequirements, + opengl: &'a GlAttributes<&'a Context>, + ) -> Result<ContextPrototype<'a>, CreationError> + { + if opengl.sharing.is_some() { + unimplemented!() + } + + // calling `eglGetDisplay` or equivalent + let display = unsafe { egl::GetDisplay(ptr::null_mut()) }; + + if display.is_null() { + return Err(CreationError::PlatformSpecific("Could not create EGL display object".to_string())); + } + + let egl_version = unsafe { + let mut major: ffi::egl::types::EGLint = 0; // out param + let mut minor: ffi::egl::types::EGLint = 0; // out param + + if egl::Initialize(display, &mut major, &mut minor) == 0 { + return Err(CreationError::OsError(format!("eglInitialize failed"))) + } + + (major, minor) + }; + + // the list of extensions supported by the client once initialized is different from the + // list of extensions obtained earlier + let extensions = if egl_version >= (1, 2) { + let p = unsafe { CStr::from_ptr(egl::QueryString(display, ffi::egl::EXTENSIONS as i32)) }; + let list = String::from_utf8(p.to_bytes().to_vec()).unwrap_or_else(|_| format!("")); + list.split(' ').map(|e| e.to_string()).collect::<Vec<_>>() + + } else { + vec![] + }; + + // binding the right API and choosing the version + let (version, api) = unsafe { + match opengl.version { + GlRequest::Latest => { + if egl_version >= (1, 4) { + if egl::BindAPI(ffi::egl::OPENGL_API) != 0 { + (None, Api::OpenGl) + } else if egl::BindAPI(ffi::egl::OPENGL_ES_API) != 0 { + (None, Api::OpenGlEs) + } else { + return Err(CreationError::OpenGlVersionNotSupported); + } + } else { + (None, Api::OpenGlEs) + } + }, + GlRequest::Specific(Api::OpenGlEs, version) => { + if egl_version >= (1, 2) { + if egl::BindAPI(ffi::egl::OPENGL_ES_API) == 0 { + return Err(CreationError::OpenGlVersionNotSupported); + } + } + (Some(version), Api::OpenGlEs) + }, + GlRequest::Specific(Api::OpenGl, version) => { + if egl_version < (1, 4) { + return Err(CreationError::OpenGlVersionNotSupported); + } + if egl::BindAPI(ffi::egl::OPENGL_API) == 0 { + return Err(CreationError::OpenGlVersionNotSupported); + } + (Some(version), Api::OpenGl) + }, + GlRequest::Specific(_, _) => return Err(CreationError::OpenGlVersionNotSupported), + GlRequest::GlThenGles { opengles_version, opengl_version } => { + if egl_version >= (1, 4) { + if egl::BindAPI(ffi::egl::OPENGL_API) != 0 { + (Some(opengl_version), Api::OpenGl) + } else if egl::BindAPI(ffi::egl::OPENGL_ES_API) != 0 { + (Some(opengles_version), Api::OpenGlEs) + } else { + return Err(CreationError::OpenGlVersionNotSupported); + } + } else { + (Some(opengles_version), Api::OpenGlEs) + } + }, + } + }; + + let (config_id, pixel_format) = unsafe { + choose_fbconfig(display, &egl_version, api, version, pf_reqs)? + }; + + Ok(ContextPrototype { + opengl: opengl, + display: display, + egl_version: egl_version, + extensions: extensions, + api: api, + version: version, + config_id: config_id, + pixel_format: pixel_format, + }) + } + + #[inline] + pub fn swap_buffers(&self) -> Result<(), ContextError> { + if self.surface.get() == ffi::egl::NO_SURFACE { + return Err(ContextError::ContextLost); + } + + let ret = unsafe { + egl::SwapBuffers(self.display, self.surface.get()) + }; + + if ret == 0 { + match unsafe { egl::GetError() } as u32 { + ffi::egl::CONTEXT_LOST => return Err(ContextError::ContextLost), + err => panic!("eglSwapBuffers failed (eglGetError returned 0x{:x})", err) + } + + } else { + Ok(()) + } + } + + pub unsafe fn make_current(&self) -> Result<(), ContextError> { + let ret = egl::MakeCurrent(self.display, self.surface.get(), self.surface.get(), self.context); + + if ret == 0 { + match egl::GetError() as u32 { + ffi::egl::CONTEXT_LOST => return Err(ContextError::ContextLost), + err => panic!("eglMakeCurrent failed (eglGetError returned 0x{:x})", err) + } + + } else { + Ok(()) + } + } + + #[inline] + pub fn is_current(&self) -> bool { + unsafe { egl::GetCurrentContext() == self.context } + } + + pub fn get_proc_address(&self, addr: &str) -> *const () { + let addr = CString::new(addr.as_bytes()).unwrap(); + let addr = addr.as_ptr(); + unsafe { + egl::GetProcAddress(addr) as *const _ + } + } + + #[inline] + pub fn get_api(&self) -> Api { + self.api + } + + #[inline] + pub fn get_pixel_format(&self) -> PixelFormat { + self.pixel_format.clone() + } +} + +unsafe impl Send for Context {} +unsafe impl Sync for Context {} + +impl Drop for Context { + fn drop(&mut self) { + unsafe { + // we don't call MakeCurrent(0, 0) because we are not sure that the context + // is still the current one + egl::DestroyContext(self.display, self.context); + egl::DestroySurface(self.display, self.surface.get()); + egl::Terminate(self.display); + } + } +} + +pub struct ContextPrototype<'a> { + opengl: &'a GlAttributes<&'a Context>, + display: ffi::egl::types::EGLDisplay, + egl_version: (ffi::egl::types::EGLint, ffi::egl::types::EGLint), + extensions: Vec<String>, + api: Api, + version: Option<(u8, u8)>, + config_id: ffi::egl::types::EGLConfig, + pixel_format: PixelFormat, +} + +impl<'a> ContextPrototype<'a> { + pub fn get_native_visual_id(&self) -> ffi::egl::types::EGLint { + let mut value = 0; + let ret = unsafe { egl::GetConfigAttrib(self.display, self.config_id, + ffi::egl::NATIVE_VISUAL_ID + as ffi::egl::types::EGLint, &mut value) }; + if ret == 0 { panic!("eglGetConfigAttrib failed") }; + value + } + + pub fn finish(self, native_window: ffi::EGLNativeWindowType) + -> Result<Context, CreationError> + { + let surface = unsafe { + let surface = egl::CreateWindowSurface(self.display, self.config_id, native_window, + ptr::null()); + if surface.is_null() { + return Err(CreationError::OsError(format!("eglCreateWindowSurface failed"))) + } + surface + }; + + self.finish_impl(surface) + } + + pub fn finish_pbuffer(self, dimensions: (u32, u32)) -> Result<Context, CreationError> { + let attrs = &[ + ffi::egl::WIDTH as c_int, dimensions.0 as c_int, + ffi::egl::HEIGHT as c_int, dimensions.1 as c_int, + ffi::egl::NONE as c_int, + ]; + + let surface = unsafe { + let surface = egl::CreatePbufferSurface(self.display, self.config_id, + attrs.as_ptr()); + if surface.is_null() { + return Err(CreationError::OsError(format!("eglCreatePbufferSurface failed"))) + } + surface + }; + + self.finish_impl(surface) + } + + fn finish_impl(self, surface: ffi::egl::types::EGLSurface) + -> Result<Context, CreationError> + { + let context = unsafe { + if let Some(version) = self.version { + create_context(self.display, &self.egl_version, + &self.extensions, self.api, version, self.config_id, + self.opengl.debug, self.opengl.robustness)? + + } else if self.api == Api::OpenGlEs { + if let Ok(ctxt) = create_context(self.display, &self.egl_version, + &self.extensions, self.api, (2, 0), self.config_id, + self.opengl.debug, self.opengl.robustness) + { + ctxt + } else if let Ok(ctxt) = create_context(self.display, &self.egl_version, + &self.extensions, self.api, (1, 0), + self.config_id, self.opengl.debug, + self.opengl.robustness) + { + ctxt + } else { + return Err(CreationError::OpenGlVersionNotSupported); + } + + } else { + if let Ok(ctxt) = create_context(self.display, &self.egl_version, + &self.extensions, self.api, (3, 2), self.config_id, + self.opengl.debug, self.opengl.robustness) + { + ctxt + } else if let Ok(ctxt) = create_context(self.display, &self.egl_version, + &self.extensions, self.api, (3, 1), + self.config_id, self.opengl.debug, + self.opengl.robustness) + { + ctxt + } else if let Ok(ctxt) = create_context(self.display, &self.egl_version, + &self.extensions, self.api, (1, 0), + self.config_id, self.opengl.debug, + self.opengl.robustness) + { + ctxt + } else { + return Err(CreationError::OpenGlVersionNotSupported); + } + } + }; + + Ok(Context { + display: self.display, + context: context, + surface: Cell::new(surface), + api: self.api, + pixel_format: self.pixel_format, + }) + } +} + +unsafe fn choose_fbconfig(display: ffi::egl::types::EGLDisplay, + egl_version: &(ffi::egl::types::EGLint, ffi::egl::types::EGLint), + api: Api, version: Option<(u8, u8)>, reqs: &PixelFormatRequirements) + -> Result<(ffi::egl::types::EGLConfig, PixelFormat), CreationError> +{ + let descriptor = { + let mut out: Vec<c_int> = Vec::with_capacity(37); + + if egl_version >= &(1, 2) { + out.push(ffi::egl::COLOR_BUFFER_TYPE as c_int); + out.push(ffi::egl::RGB_BUFFER as c_int); + } + + out.push(ffi::egl::SURFACE_TYPE as c_int); + // TODO: Some versions of Mesa report a BAD_ATTRIBUTE error + // if we ask for PBUFFER_BIT as well as WINDOW_BIT + out.push((ffi::egl::WINDOW_BIT) as c_int); + + match (api, version) { + (Api::OpenGlEs, Some((3, _))) => { + if egl_version < &(1, 3) { return Err(CreationError::NoAvailablePixelFormat); } + out.push(ffi::egl::RENDERABLE_TYPE as c_int); + out.push(ffi::egl::OPENGL_ES3_BIT as c_int); + out.push(ffi::egl::CONFORMANT as c_int); + out.push(ffi::egl::OPENGL_ES3_BIT as c_int); + }, + (Api::OpenGlEs, Some((2, _))) => { + if egl_version < &(1, 3) { return Err(CreationError::NoAvailablePixelFormat); } + out.push(ffi::egl::RENDERABLE_TYPE as c_int); + out.push(ffi::egl::OPENGL_ES2_BIT as c_int); + out.push(ffi::egl::CONFORMANT as c_int); + out.push(ffi::egl::OPENGL_ES2_BIT as c_int); + }, + (Api::OpenGlEs, Some((1, _))) => { + if egl_version >= &(1, 3) { + out.push(ffi::egl::RENDERABLE_TYPE as c_int); + out.push(ffi::egl::OPENGL_ES_BIT as c_int); + out.push(ffi::egl::CONFORMANT as c_int); + out.push(ffi::egl::OPENGL_ES_BIT as c_int); + } + }, + (Api::OpenGlEs, _) => unimplemented!(), + (Api::OpenGl, _) => { + if egl_version < &(1, 3) { return Err(CreationError::NoAvailablePixelFormat); } + out.push(ffi::egl::RENDERABLE_TYPE as c_int); + out.push(ffi::egl::OPENGL_BIT as c_int); + out.push(ffi::egl::CONFORMANT as c_int); + out.push(ffi::egl::OPENGL_BIT as c_int); + }, + (_, _) => unimplemented!(), + }; + + if let Some(hardware_accelerated) = reqs.hardware_accelerated { + out.push(ffi::egl::CONFIG_CAVEAT as c_int); + out.push(if hardware_accelerated { + ffi::egl::NONE as c_int + } else { + ffi::egl::SLOW_CONFIG as c_int + }); + } + + if let Some(color) = reqs.color_bits { + out.push(ffi::egl::RED_SIZE as c_int); + out.push((color / 3) as c_int); + out.push(ffi::egl::GREEN_SIZE as c_int); + out.push((color / 3 + if color % 3 != 0 { 1 } else { 0 }) as c_int); + out.push(ffi::egl::BLUE_SIZE as c_int); + out.push((color / 3 + if color % 3 == 2 { 1 } else { 0 }) as c_int); + } + + if let Some(alpha) = reqs.alpha_bits { + out.push(ffi::egl::ALPHA_SIZE as c_int); + out.push(alpha as c_int); + } + + if let Some(depth) = reqs.depth_bits { + out.push(ffi::egl::DEPTH_SIZE as c_int); + out.push(depth as c_int); + } + + if let Some(stencil) = reqs.stencil_bits { + out.push(ffi::egl::STENCIL_SIZE as c_int); + out.push(stencil as c_int); + } + + if let Some(true) = reqs.double_buffer { + return Err(CreationError::NoAvailablePixelFormat); + } + + if let Some(multisampling) = reqs.multisampling { + out.push(ffi::egl::SAMPLES as c_int); + out.push(multisampling as c_int); + } + + if reqs.stereoscopy { + return Err(CreationError::NoAvailablePixelFormat); + } + + // FIXME: srgb is not taken into account + + match reqs.release_behavior { + ReleaseBehavior::Flush => (), + ReleaseBehavior::None => { + // TODO: with EGL you need to manually set the behavior + unimplemented!() + }, + } + + out.push(ffi::egl::NONE as c_int); + out + }; + + // calling `eglChooseConfig` + let mut config_id = ptr::null(); // out param + let mut num_configs = 0; // out param + if egl::ChooseConfig(display, descriptor.as_ptr(), &mut config_id, 1, &mut num_configs) == 0 { + return Err(CreationError::OsError(format!("eglChooseConfig failed"))); + } + if num_configs == 0 { + return Err(CreationError::NoAvailablePixelFormat); + } + + // analyzing each config + macro_rules! attrib { + ($display:expr, $config:expr, $attr:expr) => ( + { + let mut value = 0; // out param + let res = egl::GetConfigAttrib($display, $config, + $attr as ffi::egl::types::EGLint, &mut value); + if res == 0 { + return Err(CreationError::OsError(format!("eglGetConfigAttrib failed"))); + } + value + } + ) + } + + let desc = PixelFormat { + hardware_accelerated: attrib!(display, config_id, ffi::egl::CONFIG_CAVEAT) + != ffi::egl::SLOW_CONFIG as i32, + color_bits: attrib!(display, config_id, ffi::egl::RED_SIZE) as u8 + + attrib!(display, config_id, ffi::egl::BLUE_SIZE) as u8 + + attrib!(display, config_id, ffi::egl::GREEN_SIZE) as u8, + alpha_bits: attrib!(display, config_id, ffi::egl::ALPHA_SIZE) as u8, + depth_bits: attrib!(display, config_id, ffi::egl::DEPTH_SIZE) as u8, + stencil_bits: attrib!(display, config_id, ffi::egl::STENCIL_SIZE) as u8, + stereoscopy: false, + double_buffer: true, + multisampling: match attrib!(display, config_id, ffi::egl::SAMPLES) { + 0 | 1 => None, + a => Some(a as u16), + }, + srgb: false, // TODO: use EGL_KHR_gl_colorspace to know that + }; + + Ok((config_id, desc)) +} + +unsafe fn create_context(display: ffi::egl::types::EGLDisplay, + egl_version: &(ffi::egl::types::EGLint, ffi::egl::types::EGLint), + extensions: &[String], api: Api, version: (u8, u8), + config_id: ffi::egl::types::EGLConfig, gl_debug: bool, + gl_robustness: Robustness) + -> Result<ffi::egl::types::EGLContext, CreationError> +{ + let mut context_attributes = Vec::with_capacity(10); + let mut flags = 0; + + if egl_version >= &(1, 5) || extensions.iter().find(|s| s == &"EGL_KHR_create_context") + .is_some() + { + context_attributes.push(ffi::egl::CONTEXT_MAJOR_VERSION as i32); + context_attributes.push(version.0 as i32); + context_attributes.push(ffi::egl::CONTEXT_MINOR_VERSION as i32); + context_attributes.push(version.1 as i32); + + // handling robustness + let supports_robustness = egl_version >= &(1, 5) || + extensions.iter() + .find(|s| s == &"EGL_EXT_create_context_robustness") + .is_some(); + + match gl_robustness { + Robustness::NotRobust => (), + + Robustness::NoError => { + if extensions.iter().find(|s| s == &"EGL_KHR_create_context_no_error").is_some() { + context_attributes.push(ffi::egl::CONTEXT_OPENGL_NO_ERROR_KHR as c_int); + context_attributes.push(1); + } + }, + + Robustness::RobustNoResetNotification => { + if supports_robustness { + context_attributes.push(ffi::egl::CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY + as c_int); + context_attributes.push(ffi::egl::NO_RESET_NOTIFICATION as c_int); + flags = flags | ffi::egl::CONTEXT_OPENGL_ROBUST_ACCESS as c_int; + } else { + return Err(CreationError::RobustnessNotSupported); + } + }, + + Robustness::TryRobustNoResetNotification => { + if supports_robustness { + context_attributes.push(ffi::egl::CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY + as c_int); + context_attributes.push(ffi::egl::NO_RESET_NOTIFICATION as c_int); + flags = flags | ffi::egl::CONTEXT_OPENGL_ROBUST_ACCESS as c_int; + } + }, + + Robustness::RobustLoseContextOnReset => { + if supports_robustness { + context_attributes.push(ffi::egl::CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY + as c_int); + context_attributes.push(ffi::egl::LOSE_CONTEXT_ON_RESET as c_int); + flags = flags | ffi::egl::CONTEXT_OPENGL_ROBUST_ACCESS as c_int; + } else { + return Err(CreationError::RobustnessNotSupported); + } + }, + + Robustness::TryRobustLoseContextOnReset => { + if supports_robustness { + context_attributes.push(ffi::egl::CONTEXT_OPENGL_RESET_NOTIFICATION_STRATEGY + as c_int); + context_attributes.push(ffi::egl::LOSE_CONTEXT_ON_RESET as c_int); + flags = flags | ffi::egl::CONTEXT_OPENGL_ROBUST_ACCESS as c_int; + } + }, + } + + if gl_debug { + if egl_version >= &(1, 5) { + context_attributes.push(ffi::egl::CONTEXT_OPENGL_DEBUG as i32); + context_attributes.push(ffi::egl::TRUE as i32); + } + + // TODO: using this flag sometimes generates an error + // there was a change in the specs that added this flag, so it may not be + // supported everywhere ; however it is not possible to know whether it is + // supported or not + //flags = flags | ffi::egl::CONTEXT_OPENGL_DEBUG_BIT_KHR as i32; + } + + context_attributes.push(ffi::egl::CONTEXT_FLAGS_KHR as i32); + context_attributes.push(flags); + + } else if egl_version >= &(1, 3) && api == Api::OpenGlEs { + // robustness is not supported + match gl_robustness { + Robustness::RobustNoResetNotification | Robustness::RobustLoseContextOnReset => { + return Err(CreationError::RobustnessNotSupported); + }, + _ => () + } + + context_attributes.push(ffi::egl::CONTEXT_CLIENT_VERSION as i32); + context_attributes.push(version.0 as i32); + } + + context_attributes.push(ffi::egl::NONE as i32); + + let context = egl::CreateContext(display, config_id, ptr::null(), + context_attributes.as_ptr()); + + if context.is_null() { + match egl::GetError() as u32 { + ffi::egl::BAD_ATTRIBUTE => return Err(CreationError::OpenGlVersionNotSupported), + e => panic!("eglCreateContext failed: 0x{:x}", e), + } + } + + Ok(context) +} + diff --git a/gfx/wr/wrench/src/main.rs b/gfx/wr/wrench/src/main.rs new file mode 100644 index 0000000000..d7280f9545 --- /dev/null +++ b/gfx/wr/wrench/src/main.rs @@ -0,0 +1,1040 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#[macro_use] +extern crate clap; +#[macro_use] +extern crate log; +#[macro_use] +extern crate serde; +#[macro_use] +extern crate tracy_rs; + +mod angle; +mod blob; +mod egl; +mod parse_function; +mod perf; +mod png; +mod premultiply; +mod rawtest; +mod reftest; +mod test_invalidation; +mod test_shaders; +mod wrench; +mod yaml_frame_reader; +mod yaml_helper; + +use gleam::gl; +#[cfg(feature = "software")] +use gleam::gl::Gl; +use crate::perf::PerfHarness; +use crate::rawtest::RawtestHarness; +use crate::reftest::{ReftestHarness, ReftestOptions}; +#[cfg(feature = "headless")] +use std::ffi::CString; +#[cfg(feature = "headless")] +use std::mem; +use std::os::raw::c_void; +use std::path::{Path, PathBuf}; +use std::process; +use std::ptr; +use std::rc::Rc; +#[cfg(feature = "software")] +use std::slice; +use std::sync::mpsc::{channel, Sender, Receiver}; +use webrender::DebugFlags; +use webrender::api::*; +use webrender::render_api::*; +use webrender::api::units::*; +use winit::dpi::{LogicalPosition, LogicalSize}; +use winit::event::VirtualKeyCode; +use winit::platform::run_return::EventLoopExtRunReturn; +use crate::wrench::{CapturedSequence, Wrench, WrenchThing}; +use crate::yaml_frame_reader::YamlFrameReader; + +pub const PLATFORM_DEFAULT_FACE_NAME: &str = "Arial"; + +pub static mut CURRENT_FRAME_NUMBER: u32 = 0; + +#[cfg(feature = "headless")] +pub struct HeadlessContext { + width: i32, + height: i32, + _context: osmesa_sys::OSMesaContext, + _buffer: Vec<u32>, +} + +#[cfg(not(feature = "headless"))] +pub struct HeadlessContext { + width: i32, + height: i32, +} + +impl HeadlessContext { + #[cfg(feature = "headless")] + fn new(width: i32, height: i32) -> Self { + let mut attribs = Vec::new(); + + attribs.push(osmesa_sys::OSMESA_PROFILE); + attribs.push(osmesa_sys::OSMESA_CORE_PROFILE); + attribs.push(osmesa_sys::OSMESA_CONTEXT_MAJOR_VERSION); + attribs.push(3); + attribs.push(osmesa_sys::OSMESA_CONTEXT_MINOR_VERSION); + attribs.push(3); + attribs.push(osmesa_sys::OSMESA_DEPTH_BITS); + attribs.push(24); + attribs.push(0); + + let context = + unsafe { osmesa_sys::OSMesaCreateContextAttribs(attribs.as_ptr(), ptr::null_mut()) }; + + assert!(!context.is_null()); + + let mut buffer = vec![0; (width * height) as usize]; + + unsafe { + let ret = osmesa_sys::OSMesaMakeCurrent( + context, + buffer.as_mut_ptr() as *mut _, + gl::UNSIGNED_BYTE, + width, + height, + ); + assert!(ret != 0); + }; + + HeadlessContext { + width, + height, + _context: context, + _buffer: buffer, + } + } + + #[cfg(not(feature = "headless"))] + fn new(width: i32, height: i32) -> Self { + HeadlessContext { width, height } + } + + #[cfg(feature = "headless")] + fn get_proc_address(s: &str) -> *const c_void { + let c_str = CString::new(s).expect("Unable to create CString"); + unsafe { mem::transmute(osmesa_sys::OSMesaGetProcAddress(c_str.as_ptr())) } + } + + #[cfg(not(feature = "headless"))] + fn get_proc_address(_: &str) -> *const c_void { + ptr::null() as *const _ + } +} + +#[cfg(not(feature = "software"))] +mod swgl { + pub struct Context; +} + +pub enum WindowWrapper { + WindowedContext(glutin::WindowedContext<glutin::PossiblyCurrent>, Rc<dyn gl::Gl>, Option<swgl::Context>), + Angle(winit::window::Window, angle::Context, Rc<dyn gl::Gl>, Option<swgl::Context>), + Headless(HeadlessContext, Rc<dyn gl::Gl>, Option<swgl::Context>), +} + +pub struct HeadlessEventIterater; + +impl WindowWrapper { + #[cfg(feature = "software")] + fn upload_software_to_native(&self) { + if matches!(*self, WindowWrapper::Headless(..)) { return } + let swgl = match self.software_gl() { + Some(swgl) => swgl, + None => return, + }; + swgl.finish(); + let gl = self.native_gl(); + let tex = gl.gen_textures(1)[0]; + gl.bind_texture(gl::TEXTURE_2D, tex); + let (data_ptr, w, h, stride) = swgl.get_color_buffer(0, true); + assert!(stride == w * 4); + let buffer = unsafe { slice::from_raw_parts(data_ptr as *const u8, w as usize * h as usize * 4) }; + gl.tex_image_2d(gl::TEXTURE_2D, 0, gl::RGBA8 as gl::GLint, w, h, 0, gl::BGRA, gl::UNSIGNED_BYTE, Some(buffer)); + let fb = gl.gen_framebuffers(1)[0]; + gl.bind_framebuffer(gl::READ_FRAMEBUFFER, fb); + gl.framebuffer_texture_2d(gl::READ_FRAMEBUFFER, gl::COLOR_ATTACHMENT0, gl::TEXTURE_2D, tex, 0); + gl.blit_framebuffer(0, 0, w, h, 0, 0, w, h, gl::COLOR_BUFFER_BIT, gl::NEAREST); + gl.delete_framebuffers(&[fb]); + gl.delete_textures(&[tex]); + gl.finish(); + } + + #[cfg(not(feature = "software"))] + fn upload_software_to_native(&self) { + } + + fn swap_buffers(&self) { + match *self { + WindowWrapper::WindowedContext(ref windowed_context, _, _) => { + windowed_context.swap_buffers().unwrap() + } + WindowWrapper::Angle(_, ref context, _, _) => context.swap_buffers().unwrap(), + WindowWrapper::Headless(_, _, _) => {} + } + } + + fn get_inner_size(&self) -> DeviceIntSize { + fn inner_size(window: &winit::window::Window) -> DeviceIntSize { + let size = window.inner_size(); + DeviceIntSize::new(size.width as i32, size.height as i32) + } + match *self { + WindowWrapper::WindowedContext(ref windowed_context, ..) => { + inner_size(windowed_context.window()) + } + WindowWrapper::Angle(ref window, ..) => inner_size(window), + WindowWrapper::Headless(ref context, ..) => DeviceIntSize::new(context.width, context.height), + } + } + + fn hidpi_factor(&self) -> f32 { + match *self { + WindowWrapper::WindowedContext(ref windowed_context, ..) => { + windowed_context.window().scale_factor() as f32 + } + WindowWrapper::Angle(ref window, ..) => window.scale_factor() as f32, + WindowWrapper::Headless(..) => 1.0, + } + } + + fn resize(&mut self, size: DeviceIntSize) { + match *self { + WindowWrapper::WindowedContext(ref mut windowed_context, ..) => { + windowed_context.window() + .set_inner_size(LogicalSize::new(size.width as f64, size.height as f64)) + }, + WindowWrapper::Angle(ref mut window, ..) => { + window.set_inner_size(LogicalSize::new(size.width as f64, size.height as f64)) + }, + WindowWrapper::Headless(..) => unimplemented!(), // requites Glutin update + } + } + + fn set_title(&mut self, title: &str) { + match *self { + WindowWrapper::WindowedContext(ref windowed_context, ..) => { + windowed_context.window().set_title(title) + } + WindowWrapper::Angle(ref window, ..) => window.set_title(title), + WindowWrapper::Headless(..) => (), + } + } + + pub fn software_gl(&self) -> Option<&swgl::Context> { + match *self { + WindowWrapper::WindowedContext(_, _, ref swgl) | + WindowWrapper::Angle(_, _, _, ref swgl) | + WindowWrapper::Headless(_, _, ref swgl) => swgl.as_ref(), + } + } + + pub fn native_gl(&self) -> &dyn gl::Gl { + match *self { + WindowWrapper::WindowedContext(_, ref gl, _) | + WindowWrapper::Angle(_, _, ref gl, _) | + WindowWrapper::Headless(_, ref gl, _) => &**gl, + } + } + + #[cfg(feature = "software")] + pub fn gl(&self) -> &dyn gl::Gl { + if let Some(swgl) = self.software_gl() { + swgl + } else { + self.native_gl() + } + } + + pub fn is_software(&self) -> bool { + self.software_gl().is_some() + } + + #[cfg(not(feature = "software"))] + pub fn gl(&self) -> &dyn gl::Gl { + self.native_gl() + } + + pub fn clone_gl(&self) -> Rc<dyn gl::Gl> { + match *self { + WindowWrapper::WindowedContext(_, ref gl, ref swgl) | + WindowWrapper::Angle(_, _, ref gl, ref swgl) | + WindowWrapper::Headless(_, ref gl, ref swgl) => { + match swgl { + #[cfg(feature = "software")] + Some(ref swgl) => Rc::new(*swgl), + None => gl.clone(), + #[cfg(not(feature = "software"))] + _ => panic!(), + } + } + } + } + + + #[cfg(feature = "software")] + fn update_software(&self, dim: DeviceIntSize) { + if let Some(swgl) = self.software_gl() { + swgl.init_default_framebuffer(0, 0, dim.width, dim.height, 0, std::ptr::null_mut()); + } + } + + #[cfg(not(feature = "software"))] + fn update_software(&self, _dim: DeviceIntSize) { + } + + fn update(&self, wrench: &mut Wrench) { + let dim = self.get_inner_size(); + self.update_software(dim); + wrench.update(dim); + } +} + +#[cfg(feature = "software")] +fn make_software_context() -> swgl::Context { + let ctx = swgl::Context::create(); + ctx.make_current(); + ctx +} + +#[cfg(not(feature = "software"))] +fn make_software_context() -> swgl::Context { + panic!("software feature not enabled") +} + +fn make_window( + size: DeviceIntSize, + vsync: bool, + events_loop: &Option<winit::event_loop::EventLoop<()>>, + angle: bool, + gl_request: glutin::GlRequest, + software: bool, +) -> WindowWrapper { + let sw_ctx = if software { + Some(make_software_context()) + } else { + None + }; + + let wrapper = if let Some(events_loop) = events_loop { + let context_builder = glutin::ContextBuilder::new() + .with_gl(gl_request) + // Glutin can fail to create a context on Android if vsync is not set + .with_vsync(vsync || cfg!(target_os = "android")); + + let window_builder = winit::window::WindowBuilder::new() + .with_title("WRench") + .with_inner_size(LogicalSize::new(size.width as f64, size.height as f64)); + + if angle { + angle::Context::with_window( + window_builder, context_builder, events_loop + ).map(|(_window, _context)| { + unsafe { + _context + .make_current() + .expect("unable to make context current!"); + } + + let gl = match _context.get_api() { + glutin::Api::OpenGl => unsafe { + gl::GlFns::load_with(|symbol| _context.get_proc_address(symbol) as *const _) + }, + glutin::Api::OpenGlEs => unsafe { + gl::GlesFns::load_with(|symbol| _context.get_proc_address(symbol) as *const _) + }, + glutin::Api::WebGl => unimplemented!(), + }; + + WindowWrapper::Angle(_window, _context, gl, sw_ctx) + }).unwrap() + } else { + let windowed_context = context_builder + .build_windowed(window_builder, events_loop) + .unwrap(); + + let windowed_context = unsafe { + windowed_context + .make_current() + .expect("unable to make context current!") + }; + + let gl = match windowed_context.get_api() { + glutin::Api::OpenGl => unsafe { + gl::GlFns::load_with( + |symbol| windowed_context.get_proc_address(symbol) as *const _ + ) + }, + glutin::Api::OpenGlEs => unsafe { + gl::GlesFns::load_with( + |symbol| windowed_context.get_proc_address(symbol) as *const _ + ) + }, + glutin::Api::WebGl => unimplemented!(), + }; + + WindowWrapper::WindowedContext(windowed_context, gl, sw_ctx) + } + } else { + #[cfg_attr(not(feature = "software"), allow(unused_variables))] + let gl = if let Some(sw_ctx) = sw_ctx { + #[cfg(feature = "software")] + { + Rc::new(sw_ctx) + } + #[cfg(not(feature = "software"))] + { + unreachable!("make_software_context() should have failed if 'software' feature is not enabled") + } + } else { + match gl::GlType::default() { + gl::GlType::Gl => unsafe { + gl::GlFns::load_with(|symbol| { + HeadlessContext::get_proc_address(symbol) as *const _ + }) + }, + gl::GlType::Gles => unsafe { + gl::GlesFns::load_with(|symbol| { + HeadlessContext::get_proc_address(symbol) as *const _ + }) + }, + } + }; + WindowWrapper::Headless(HeadlessContext::new(size.width, size.height), gl, sw_ctx) + }; + + let gl = wrapper.gl(); + + gl.clear_color(0.3, 0.0, 0.0, 1.0); + + let gl_version = gl.get_string(gl::VERSION); + let gl_renderer = gl.get_string(gl::RENDERER); + + println!("OpenGL version {}, {}", gl_version, gl_renderer); + println!( + "hidpi factor: {}", + wrapper.hidpi_factor() + ); + + wrapper +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum NotifierEvent { + WakeUp { + composite_needed: bool, + }, + ShutDown, +} + +struct Notifier { + tx: Sender<NotifierEvent>, +} + +// setup a notifier so we can wait for frames to be finished +impl RenderNotifier for Notifier { + fn clone(&self) -> Box<dyn RenderNotifier> { + Box::new(Notifier { + tx: self.tx.clone(), + }) + } + + fn wake_up( + &self, + composite_needed: bool, + ) { + let msg = NotifierEvent::WakeUp { + composite_needed, + }; + self.tx.send(msg).unwrap(); + } + + fn shut_down(&self) { + self.tx.send(NotifierEvent::ShutDown).unwrap(); + } + + fn new_frame_ready(&self, + _: DocumentId, + _scrolled: bool, + composite_needed: bool, + _: FramePublishId) { + // TODO(gw): Refactor wrench so that it can take advantage of cases + // where no composite is required when appropriate. + self.wake_up(composite_needed); + } +} + +fn create_notifier() -> (Box<dyn RenderNotifier>, Receiver<NotifierEvent>) { + let (tx, rx) = channel(); + (Box::new(Notifier { tx }), rx) +} + +fn rawtest(mut wrench: Wrench, window: &mut WindowWrapper, rx: Receiver<NotifierEvent>) { + RawtestHarness::new(&mut wrench, window, &rx).run(); + wrench.shut_down(rx); +} + +fn reftest<'a>( + mut wrench: Wrench, + window: &mut WindowWrapper, + subargs: &clap::ArgMatches, + rx: Receiver<NotifierEvent> +) -> usize { + let dim = window.get_inner_size(); + #[cfg(target_os = "android")] + let base_manifest = { + let mut list_path = PathBuf::new(); + list_path.push(ndk_glue::native_activity().internal_data_path().to_str().unwrap()); + list_path.push("wrench"); + list_path.push("reftests"); + list_path.push("reftest.list"); + list_path + }; + #[cfg(not(target_os = "android"))] + let base_manifest = Path::new("reftests/reftest.list").to_owned(); + + let specific_reftest = subargs.value_of("REFTEST").map(Path::new); + let mut reftest_options = ReftestOptions::default(); + if let Some(allow_max_diff) = subargs.value_of("fuzz_tolerance") { + reftest_options.allow_max_difference = allow_max_diff.parse().unwrap_or(1); + reftest_options.allow_num_differences = dim.width as usize * dim.height as usize; + } + let num_failures = ReftestHarness::new(&mut wrench, window, &rx) + .run(&base_manifest, specific_reftest, &reftest_options); + wrench.shut_down(rx); + num_failures +} + +#[cfg_attr(target_os = "android", ndk_glue::main)] +pub fn main() { + #[cfg(feature = "env_logger")] + env_logger::init(); + + // By default on Android, the ndk_glue crate will redirect stdout and stderr to logcat. Logcat, + // however, truncates long lines, meaning our base64 image dumps will be truncated. To avoid + // this, copy ndk_glue's code to redirect stdout and stderr to logcat, but additionally write + // it to a file which can later be pulled from the device. + #[cfg(target_os = "android")] + { + use std::ffi::{CStr, CString}; + use std::fs::File; + use std::io::{BufRead, BufReader, Write}; + use std::os::unix::io::{FromRawFd, RawFd}; + use std::thread; + + let mut out_path = PathBuf::new(); + out_path.push(ndk_glue::native_activity().internal_data_path().to_str().unwrap()); + out_path.push("wrench"); + out_path.push("stdout"); + let mut out_file = File::create(&out_path).expect("Failed to create stdout file"); + + let mut logpipe: [RawFd; 2] = Default::default(); + unsafe { + libc::pipe(logpipe.as_mut_ptr()); + libc::dup2(logpipe[1], libc::STDOUT_FILENO); + libc::dup2(logpipe[1], libc::STDERR_FILENO); + } + + thread::spawn(move || { + let tag = CStr::from_bytes_with_nul(b"Wrench\0").unwrap(); + let mut reader = BufReader::new(unsafe { File::from_raw_fd(logpipe[0]) }); + let mut buffer = String::new(); + loop { + buffer.clear(); + if let Ok(len) = reader.read_line(&mut buffer) { + if len == 0 { + break; + } else if let Ok(msg) = CString::new(buffer.clone()) { + out_file.write_all(msg.as_bytes()).ok(); + ndk_glue::android_log(log::Level::Info, tag, &msg); + } + } + } + }); + } + + #[cfg(target_os = "macos")] + { + use core_foundation::{self as cf, base::TCFType}; + let i = cf::bundle::CFBundle::main_bundle().info_dictionary(); + let mut i = unsafe { i.to_mutable() }; + i.set( + cf::string::CFString::new("NSSupportsAutomaticGraphicsSwitching"), + cf::boolean::CFBoolean::true_value().into_CFType(), + ); + } + + #[allow(deprecated)] // FIXME(bug 1771450): Use clap-serde or another way + let args_yaml = load_yaml!("args.yaml"); + #[allow(deprecated)] // FIXME(bug 1771450): Use clap-serde or another way + let clap = clap::Command::from_yaml(args_yaml) + .arg_required_else_help(true); + + // On android devices, attempt to read command line arguments from a text + // file located at <internal_data_dir>/wrench/args. + #[cfg(target_os = "android")] + let args = { + // get full backtraces by default because it's hard to request + // externally on android + std::env::set_var("RUST_BACKTRACE", "full"); + + let mut args = vec!["wrench".to_string()]; + + let mut args_path = PathBuf::new(); + args_path.push(ndk_glue::native_activity().internal_data_path().to_str().unwrap()); + args_path.push("wrench"); + args_path.push("args"); + + if let Ok(wrench_args) = std::fs::read_to_string(&args_path) { + for line in wrench_args.lines() { + if let Some(envvar) = line.strip_prefix("env: ") { + if let Some((lhs, rhs)) = envvar.split_once('=') { + std::env::set_var(lhs, rhs); + } else { + std::env::set_var(envvar, ""); + } + + continue; + } + for arg in line.split_whitespace() { + args.push(arg.to_string()); + } + } + } + + clap.get_matches_from(&args) + }; + + #[cfg(not(target_os = "android"))] + let args = clap.get_matches(); + + // handle some global arguments + let res_path = args.value_of("shaders").map(PathBuf::from); + let size = args.value_of("size") + .map(|s| if s == "720p" { + DeviceIntSize::new(1280, 720) + } else if s == "1080p" { + DeviceIntSize::new(1920, 1080) + } else if s == "4k" { + DeviceIntSize::new(3840, 2160) + } else { + let x = s.find('x').expect( + "Size must be specified exactly as 720p, 1080p, 4k, or width x height", + ); + let w = s[0 .. x].parse::<i32>().expect("Invalid size width"); + let h = s[x + 1 ..].parse::<i32>().expect("Invalid size height"); + DeviceIntSize::new(w, h) + }) + .unwrap_or(DeviceIntSize::new(1920, 1080)); + + let dump_shader_source = args.value_of("dump_shader_source").map(String::from); + + let mut events_loop = if args.is_present("headless") { + None + } else { + Some(winit::event_loop::EventLoop::new()) + }; + + let gl_request = match args.value_of("renderer") { + Some("es3") => { + glutin::GlRequest::Specific(glutin::Api::OpenGlEs, (3, 0)) + } + Some("gl3") => { + glutin::GlRequest::Specific(glutin::Api::OpenGl, (3, 2)) + } + Some("default") | None => { + glutin::GlRequest::GlThenGles { + opengl_version: (3, 2), + opengles_version: (3, 0), + } + } + Some(api) => { + panic!("Unexpected renderer string {}", api); + } + }; + + let software = args.is_present("software"); + + // On Android we can only create an OpenGL context when we have a + // native_window handle, so wait here until we are resumed and have a + // handle. If the app gets minimized this will no longer be valid, but + // that's okay for wrench's usage. + #[cfg(target_os = "android")] + { + events_loop.as_mut().unwrap().run_return(|event, _elwt, control_flow| { + if let winit::event::Event::Resumed = event { + if ndk_glue::native_window().is_some() { + *control_flow = winit::event_loop::ControlFlow::Exit; + } + } + }); + } + + let mut window = make_window( + size, + args.is_present("vsync"), + &events_loop, + args.is_present("angle"), + gl_request, + software, + ); + let dim = window.get_inner_size(); + + let needs_frame_notifier = args.subcommand_name().map_or(false, |name| { + ["perf", "reftest", "png", "rawtest", "test_invalidation"].contains(&name) + }); + let (notifier, rx) = if needs_frame_notifier { + let (notifier, rx) = create_notifier(); + (Some(notifier), Some(rx)) + } else { + (None, None) + }; + + let mut wrench = Wrench::new( + &mut window, + events_loop.as_mut().map(|el| el.create_proxy()), + res_path, + !args.is_present("use_unoptimized_shaders"), + dim, + args.is_present("rebuild"), + args.is_present("no_subpixel_aa"), + args.is_present("verbose"), + args.is_present("no_scissor"), + args.is_present("no_batch"), + args.is_present("precache"), + dump_shader_source, + notifier, + ); + + if let Some(ui_str) = args.value_of("profiler_ui") { + wrench.renderer.set_profiler_ui(ui_str); + } + + window.update(&mut wrench); + + if let Some(window_title) = wrench.take_title() { + if !cfg!(windows) { + window.set_title(&window_title); + } + } + + if let Some(subargs) = args.subcommand_matches("show") { + let no_block = args.is_present("no_block"); + let no_batch = args.is_present("no_batch"); + render( + &mut wrench, + &mut window, + events_loop.as_mut().expect("`wrench show` is not supported in headless mode"), + subargs, + no_block, + no_batch, + ); + } else if let Some(subargs) = args.subcommand_matches("png") { + let surface = match subargs.value_of("surface") { + Some("screen") | None => png::ReadSurface::Screen, + Some("gpu-cache") => png::ReadSurface::GpuCache, + _ => panic!("Unknown surface argument value") + }; + let output_path = subargs.value_of("OUTPUT").map(PathBuf::from); + let reader = YamlFrameReader::new_from_args(subargs); + png::png(&mut wrench, surface, &mut window, reader, rx.unwrap(), output_path); + } else if let Some(subargs) = args.subcommand_matches("reftest") { + // Exit with an error code in order to ensure the CI job fails. + process::exit(reftest(wrench, &mut window, subargs, rx.unwrap()) as _); + } else if args.subcommand_matches("rawtest").is_some() { + rawtest(wrench, &mut window, rx.unwrap()); + return; + } else if let Some(subargs) = args.subcommand_matches("perf") { + // Perf mode wants to benchmark the total cost of drawing + // a new displaty list each frame. + wrench.rebuild_display_lists = true; + + let as_csv = subargs.is_present("csv"); + let auto_filename = subargs.is_present("auto-filename"); + + let warmup_frames = subargs.value_of("warmup_frames").map(|s| s.parse().unwrap()); + let sample_count = subargs.value_of("sample_count").map(|s| s.parse().unwrap()); + + let harness = PerfHarness::new(&mut wrench, + &mut window, + rx.unwrap(), + warmup_frames, + sample_count); + + let benchmark = subargs.value_of("benchmark").unwrap_or("benchmarks/benchmarks.list"); + println!("Benchmark: {}", benchmark); + let base_manifest = Path::new(benchmark); + + let mut filename = subargs.value_of("filename").unwrap().to_string(); + if auto_filename { + let timestamp = chrono::Local::now().format("%Y-%m-%d-%H-%M-%S"); + filename.push_str( + &format!("/wrench-perf-{}.{}", + timestamp, + if as_csv { "csv" } else { "json" })); + } + harness.run(base_manifest, &filename, as_csv); + return; + } else if args.subcommand_matches("test_invalidation").is_some() { + let harness = test_invalidation::TestHarness::new( + &mut wrench, + &mut window, + rx.unwrap(), + ); + + harness.run(); + } else if let Some(subargs) = args.subcommand_matches("compare_perf") { + let first_filename = subargs.value_of("first_filename").unwrap(); + let second_filename = subargs.value_of("second_filename").unwrap(); + perf::compare(first_filename, second_filename); + return; + } else if args.subcommand_matches("test_init").is_some() { + // Wrench::new() unwraps the Renderer initialization, so if + // we reach this point then we have initialized successfully. + println!("Initialization successful"); + } else if args.subcommand_matches("test_shaders").is_some() { + test_shaders::test_shaders(); + } else { + panic!("Should never have gotten here! {:?}", args); + }; + + wrench.renderer.deinit(); + + // On android force-exit the process otherwise it stays running forever. + #[cfg(target_os = "android")] + process::exit(0); +} + +fn render<'a>( + wrench: &mut Wrench, + window: &mut WindowWrapper, + events_loop: &mut winit::event_loop::EventLoop<()>, + subargs: &clap::ArgMatches, + no_block: bool, + no_batch: bool, +) { + let input_path = subargs.value_of("INPUT").map(PathBuf::from).unwrap(); + + // If the input is a directory, we are looking at a capture. + let mut thing = if input_path.join("scenes").as_path().is_dir() { + let scene_id = subargs.value_of("scene-id").map(|z| z.parse::<u32>().unwrap()); + let frame_id = subargs.value_of("frame-id").map(|z| z.parse::<u32>().unwrap()); + Box::new(CapturedSequence::new( + input_path, + scene_id.unwrap_or(1), + frame_id.unwrap_or(1), + )) + } else if input_path.as_path().is_dir() { + let mut documents = wrench.api.load_capture(input_path, None); + println!("loaded {:?}", documents.iter().map(|cd| cd.document_id).collect::<Vec<_>>()); + let captured = documents.swap_remove(0); + wrench.document_id = captured.document_id; + Box::new(captured) as Box<dyn WrenchThing> + } else { + match input_path.extension().and_then(std::ffi::OsStr::to_str) { + Some("yaml") => { + Box::new(YamlFrameReader::new_from_args(subargs)) as Box<dyn WrenchThing> + } + _ => panic!("Tried to render with an unknown file type."), + } + }; + + window.update(wrench); + thing.do_frame(wrench); + + if let Some(fb_size) = wrench.renderer.device_size() { + window.resize(fb_size); + } + + let mut debug_flags = DebugFlags::empty(); + debug_flags.set(DebugFlags::DISABLE_BATCHING, no_batch); + + // Default the profile overlay on for android. + if cfg!(target_os = "android") { + debug_flags.toggle(DebugFlags::PROFILER_DBG); + wrench.api.send_debug_cmd(DebugCommand::SetFlags(debug_flags)); + } + + let mut show_help = false; + let mut do_loop = false; + let mut cursor_position = WorldPoint::zero(); + let mut do_render = false; + let mut do_frame = false; + + events_loop.run_return(|event, _elwt, control_flow| { + // By default after each iteration of the event loop we block the thread until the next + // events arrive. --no-block can be used to run the event loop as quickly as possible. + // On Android, we are generally profiling when running wrench, and don't want to block + // on UI events. + if !no_block && cfg!(not(target_os = "android")) { + *control_flow = winit::event_loop::ControlFlow::Wait; + } else { + *control_flow = winit::event_loop::ControlFlow::Poll; + } + + match event { + winit::event::Event::UserEvent(_) => { + do_render = true; + } + winit::event::Event::WindowEvent { event, .. } => match event { + winit::event::WindowEvent::CloseRequested => { + *control_flow = winit::event_loop::ControlFlow::Exit; + } + winit::event::WindowEvent::Focused(..) => do_render = true, + winit::event::WindowEvent::CursorMoved { position, .. } => { + let pos: LogicalPosition<f32> = position.to_logical(window.hidpi_factor() as f64); + cursor_position = WorldPoint::new(pos.x, pos.y); + wrench.renderer.set_cursor_position( + DeviceIntPoint::new( + cursor_position.x.round() as i32, + cursor_position.y.round() as i32, + ), + ); + do_render = true; + } + winit::event::WindowEvent::KeyboardInput { + input: winit::event::KeyboardInput { + state: winit::event::ElementState::Pressed, + virtual_keycode: Some(vk), + .. + }, + .. + } => match vk { + VirtualKeyCode::Escape => { + *control_flow = winit::event_loop::ControlFlow::Exit; + } + VirtualKeyCode::B => { + debug_flags.toggle(DebugFlags::INVALIDATION_DBG); + wrench.api.send_debug_cmd(DebugCommand::SetFlags(debug_flags)); + do_render = true; + } + VirtualKeyCode::P => { + debug_flags.toggle(DebugFlags::PROFILER_DBG); + wrench.api.send_debug_cmd(DebugCommand::SetFlags(debug_flags)); + do_render = true; + } + VirtualKeyCode::O => { + debug_flags.toggle(DebugFlags::RENDER_TARGET_DBG); + wrench.api.send_debug_cmd(DebugCommand::SetFlags(debug_flags)); + do_render = true; + } + VirtualKeyCode::I => { + debug_flags.toggle(DebugFlags::TEXTURE_CACHE_DBG); + wrench.api.send_debug_cmd(DebugCommand::SetFlags(debug_flags)); + do_render = true; + } + VirtualKeyCode::D => { + debug_flags.toggle(DebugFlags::PICTURE_CACHING_DBG); + wrench.api.send_debug_cmd(DebugCommand::SetFlags(debug_flags)); + do_render = true; + } + VirtualKeyCode::Q => { + debug_flags.toggle(DebugFlags::GPU_TIME_QUERIES | DebugFlags::GPU_SAMPLE_QUERIES); + wrench.api.send_debug_cmd(DebugCommand::SetFlags(debug_flags)); + do_render = true; + } + VirtualKeyCode::V => { + debug_flags.toggle(DebugFlags::SHOW_OVERDRAW); + wrench.api.send_debug_cmd(DebugCommand::SetFlags(debug_flags)); + do_render = true; + } + VirtualKeyCode::G => { + debug_flags.toggle(DebugFlags::GPU_CACHE_DBG); + wrench.api.send_debug_cmd(DebugCommand::SetFlags(debug_flags)); + + // force scene rebuild to see the full set of used GPU cache entries + let mut txn = Transaction::new(); + txn.set_root_pipeline(wrench.root_pipeline_id); + wrench.api.send_transaction(wrench.document_id, txn); + + do_frame = true; + } + VirtualKeyCode::M => { + wrench.api.notify_memory_pressure(); + do_render = true; + } + VirtualKeyCode::L => { + do_loop = !do_loop; + do_render = true; + } + VirtualKeyCode::Left => { + thing.prev_frame(); + do_frame = true; + } + VirtualKeyCode::Right => { + thing.next_frame(); + do_frame = true; + } + VirtualKeyCode::H => { + show_help = !show_help; + do_render = true; + } + VirtualKeyCode::C => { + let path = PathBuf::from("../captures/wrench"); + wrench.api.save_capture(path, CaptureBits::all()); + } + VirtualKeyCode::X => { + let results = wrench.api.hit_test( + wrench.document_id, + cursor_position, + ); + + println!("Hit test results:"); + for item in &results.items { + println!(" • {:?}", item); + } + println!(); + } + VirtualKeyCode::Z => { + debug_flags.toggle(DebugFlags::ZOOM_DBG); + wrench.api.send_debug_cmd(DebugCommand::SetFlags(debug_flags)); + do_render = true; + } + VirtualKeyCode::Y => { + println!("Clearing all caches..."); + wrench.api.send_debug_cmd(DebugCommand::ClearCaches(ClearCache::all())); + do_frame = true; + } + _ => {} + } + _ => {} + }, + winit::event::Event::MainEventsCleared => { + window.update(wrench); + + if do_frame { + do_frame = false; + let frame_num = thing.do_frame(wrench); + unsafe { + CURRENT_FRAME_NUMBER = frame_num; + } + } + + if do_render { + do_render = false; + + if show_help { + wrench.show_onscreen_help(); + } + + wrench.render(); + window.upload_software_to_native(); + window.swap_buffers(); + + if do_loop { + thing.next_frame(); + } + } + } + _ => {} + } + }); +} diff --git a/gfx/wr/wrench/src/parse_function.rs b/gfx/wr/wrench/src/parse_function.rs new file mode 100644 index 0000000000..92040b7680 --- /dev/null +++ b/gfx/wr/wrench/src/parse_function.rs @@ -0,0 +1,134 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::str::CharIndices; + +// support arguments like '4', 'ab', '4.0', '>=10.14', '*123' +fn acceptable_arg_character(c: char) -> bool { + c.is_alphanumeric() || c == '.' || c == '-' || c == '<' || c == '>' || c == '=' || c == '*' +} + +// A crappy parser for parsing strings like "translate(1, 3) blahblah" +// Returns a tuple with three components: +// - First component is the function name (e.g. "translate") +// - Second component is the list of arguments (e.g. vec!["1", "3"]) +// - Third component is the rest of the string "blahblah" +pub fn parse_function(s: &str) -> (&str, Vec<&str>, &str) { + // XXX: This is not particularly easy to read. Sorry. + struct Parser<'a> { + itr: CharIndices<'a>, + start: usize, + o: Option<(usize, char)>, + } + impl<'a> Parser<'a> { + fn skip_whitespace(&mut self) { + while let Some(k) = self.o { + if !k.1.is_whitespace() { + break; + } + self.start = k.0 + k.1.len_utf8(); + self.o = self.itr.next(); + } + } + } + let mut c = s.char_indices(); + let o = c.next(); + let mut p = Parser { + itr: c, + start: 0, + o, + }; + + p.skip_whitespace(); + + let mut end = p.start; + while let Some(k) = p.o { + if !k.1.is_alphabetic() && k.1 != '_' && k.1 != '-' { + break; + } + end = k.0 + k.1.len_utf8(); + p.o = p.itr.next(); + } + + let name = &s[p.start .. end]; + let mut args = Vec::new(); + + p.skip_whitespace(); + + if let Some(k) = p.o { + if k.1 != '(' { + return (name, args, &s[p.start ..]); + } + p.start = k.0 + k.1.len_utf8(); + p.o = p.itr.next(); + } + + loop { + p.skip_whitespace(); + + let mut end = p.start; + let mut brackets: Vec<char> = Vec::new(); + while let Some(k) = p.o { + let prev_bracket_count = brackets.len(); + match k.1 { + '[' | '(' => brackets.push(k.1), + ']' | ')' => { + let open_bracket = match k.1 { + ']' => '[', + ')' => '(', + _ => panic!(), + }; + match brackets.pop() { + // Allow final closing ) for command invocation after args + None if k.1 == ')' => break, + Some(bracket) if bracket == open_bracket => {} + _ => panic!("Unexpected closing bracket {}", k.1), + } + } + _ => {} + } + + let not_in_bracket = brackets.is_empty() && prev_bracket_count == 0; + if !acceptable_arg_character(k.1) && not_in_bracket { + break; + } + end = k.0 + k.1.len_utf8(); + p.o = p.itr.next(); + } + + args.push(&s[p.start .. end]); + + p.skip_whitespace(); + + if let Some(k) = p.o { + p.start = k.0 + k.1.len_utf8(); + p.o = p.itr.next(); + // unless we find a comma we're done + if k.1 != ',' { + if k.1 != ')' { + panic!("Unexpected closing character: {}", k.1); + } + break; + } + } else { + break; + } + } + (name, args, &s[p.start ..]) +} + +#[test] +fn test() { + assert_eq!(parse_function("rotate(40)").0, "rotate"); + assert_eq!(parse_function(" rotate(40)").0, "rotate"); + assert_eq!(parse_function(" rotate (40)").0, "rotate"); + assert_eq!(parse_function(" rotate ( 40 )").1[0], "40"); + assert_eq!(parse_function("rotate(-40.0)").1[0], "-40.0"); + assert_eq!(parse_function("drop-shadow(0, [1, 2, 3, 4], 5)").1[0], "0"); + assert_eq!(parse_function("drop-shadow(0, [1, 2, 3, 4], 5)").1[1], "[1, 2, 3, 4]"); + assert_eq!(parse_function("drop-shadow(0, [1, 2, 3, 4], 5)").1[2], "5"); + assert_eq!(parse_function("drop-shadow(0, [1, 2, [3, 4]], 5)").1[1], "[1, 2, [3, 4]]"); + assert_eq!(parse_function("func(nest([1, 2]), [3, 4])").1[0], "nest([1, 2])"); + assert_eq!(parse_function("func(nest([1, 2]), [nest(3), nest(4)])").1[1], "[nest(3), nest(4)]"); +} diff --git a/gfx/wr/wrench/src/perf.rs b/gfx/wr/wrench/src/perf.rs new file mode 100644 index 0000000000..6b3a171408 --- /dev/null +++ b/gfx/wr/wrench/src/perf.rs @@ -0,0 +1,349 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::NotifierEvent; +use crate::WindowWrapper; +use std::collections::{HashMap, HashSet}; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::sync::mpsc::Receiver; +use crate::wrench::{Wrench, WrenchThing}; +use crate::yaml_frame_reader::YamlFrameReader; +use webrender::DebugFlags; +use webrender::render_api::DebugCommand; + +const COLOR_DEFAULT: &str = "\x1b[0m"; +const COLOR_RED: &str = "\x1b[31m"; +const COLOR_GREEN: &str = "\x1b[32m"; +const COLOR_MAGENTA: &str = "\x1b[95m"; + +const MIN_SAMPLE_COUNT: usize = 50; +const SAMPLE_EXCLUDE_COUNT: usize = 10; + +pub struct Benchmark { + pub test: PathBuf, +} + +pub struct BenchmarkManifest { + pub benchmarks: Vec<Benchmark>, +} + +impl BenchmarkManifest { + pub fn new(manifest: &Path) -> BenchmarkManifest { + let dir = manifest.parent().unwrap(); + let f = + File::open(manifest).unwrap_or_else(|_| panic!("couldn't open manifest: {}", manifest.display())); + let file = BufReader::new(&f); + + let mut benchmarks = Vec::new(); + + for line in file.lines() { + let l = line.unwrap(); + + // strip the comments + let s = &l[0 .. l.find('#').unwrap_or(l.len())]; + let s = s.trim(); + if s.is_empty() { + continue; + } + + let mut items = s.split_whitespace(); + + match items.next() { + Some("include") => { + let include = dir.join(items.next().unwrap()); + + benchmarks.append(&mut BenchmarkManifest::new(include.as_path()).benchmarks); + } + Some(name) => { + let test = dir.join(name); + benchmarks.push(Benchmark { test }); + } + _ => panic!(), + }; + } + + BenchmarkManifest { + benchmarks, + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +struct TestProfileRange { + min: u64, + avg: u64, + max: u64, +} + +#[derive(Clone, Serialize, Deserialize)] +struct TestProfile { + name: String, + backend_time_ns: TestProfileRange, + composite_time_ns: TestProfileRange, + paint_time_ns: TestProfileRange, + draw_calls: usize, +} + +impl TestProfile { + fn csv_header() -> String { + "name,\ + backend_time_ns min, avg, max,\ + composite_time_ns min, avg, max,\ + paint_time_ns min, avg, max,\ + draw_calls\n".to_string() + } + + fn convert_to_csv(&self) -> String { + format!("{},\ + {},{},{},\ + {},{},{},\ + {},{},{},\ + {}\n", + self.name, + self.backend_time_ns.min, self.backend_time_ns.avg, self.backend_time_ns.max, + self.composite_time_ns.min, self.composite_time_ns.avg, self.composite_time_ns.max, + self.paint_time_ns.min, self.paint_time_ns.avg, self.paint_time_ns.max, + self.draw_calls) + } +} + +#[derive(Serialize, Deserialize)] +struct Profile { + tests: Vec<TestProfile>, +} + +impl Profile { + fn new() -> Profile { + Profile { tests: Vec::new() } + } + + fn add(&mut self, profile: TestProfile) { + self.tests.push(profile); + } + + fn save(&self, filename: &str, as_csv: bool) { + let mut file = File::create(&filename).unwrap(); + if as_csv { + file.write_all(&TestProfile::csv_header().into_bytes()).unwrap(); + for test in &self.tests { + file.write_all(&test.convert_to_csv().into_bytes()).unwrap(); + } + } else { + let s = serde_json::to_string_pretty(self).unwrap(); + file.write_all(&s.into_bytes()).unwrap(); + file.write_all(b"\n").unwrap(); + } + } + + fn load(filename: &str) -> Profile { + let mut file = File::open(&filename).unwrap(); + let mut string = String::new(); + file.read_to_string(&mut string).unwrap(); + serde_json::from_str(&string).expect("Unable to load profile!") + } + + fn build_set_and_map_of_tests(&self) -> (HashSet<String>, HashMap<String, TestProfile>) { + let mut hash_set = HashSet::new(); + let mut hash_map = HashMap::new(); + + for test in &self.tests { + hash_set.insert(test.name.clone()); + hash_map.insert(test.name.clone(), test.clone()); + } + + (hash_set, hash_map) + } +} + +pub struct PerfHarness<'a> { + wrench: &'a mut Wrench, + window: &'a mut WindowWrapper, + rx: Receiver<NotifierEvent>, + warmup_frames: usize, + sample_count: usize, +} + +impl<'a> PerfHarness<'a> { + pub fn new(wrench: &'a mut Wrench, + window: &'a mut WindowWrapper, + rx: Receiver<NotifierEvent>, + warmup_frames: Option<usize>, + sample_count: Option<usize>) -> Self { + PerfHarness { + wrench, + window, + rx, + warmup_frames: warmup_frames.unwrap_or(0usize), + sample_count: sample_count.unwrap_or(MIN_SAMPLE_COUNT), + } + } + + pub fn run(mut self, base_manifest: &Path, filename: &str, as_csv: bool) { + let manifest = BenchmarkManifest::new(base_manifest); + + let mut profile = Profile::new(); + + for t in manifest.benchmarks { + let stats = self.render_yaml(t.test.as_path()); + profile.add(stats); + } + + profile.save(filename, as_csv); + } + + fn render_yaml(&mut self, filename: &Path) -> TestProfile { + let mut reader = YamlFrameReader::new(filename); + + // Loop until we get a reasonable number of CPU and GPU + // frame profiles. Then take the mean. + let mut cpu_frame_profiles = Vec::new(); + let mut gpu_frame_profiles = Vec::new(); + + let mut debug_flags = DebugFlags::empty(); + debug_flags.set(DebugFlags::GPU_TIME_QUERIES | DebugFlags::GPU_SAMPLE_QUERIES, true); + self.wrench.api.send_debug_cmd(DebugCommand::SetFlags(debug_flags)); + + let mut frame_count = 0; + + while cpu_frame_profiles.len() < self.sample_count || + gpu_frame_profiles.len() < self.sample_count + { + reader.do_frame(self.wrench); + self.rx.recv().unwrap(); + self.wrench.render(); + self.window.swap_buffers(); + let (cpu_profiles, gpu_profiles) = self.wrench.get_frame_profiles(); + if frame_count >= self.warmup_frames { + cpu_frame_profiles.extend(cpu_profiles); + gpu_frame_profiles.extend(gpu_profiles); + } + frame_count += 1; + } + + // Ensure the draw calls match in every sample. + let draw_calls = cpu_frame_profiles[0].draw_calls; + let draw_calls_same = + cpu_frame_profiles + .iter() + .all(|s| s.draw_calls == draw_calls); + + // this can be normal in cases where some elements are cached (eg. linear + // gradients), but print a warning in case it's not (which could make the + // benchmark produce unexpected results). + if !draw_calls_same { + println!("Warning: not every frame has the same number of draw calls"); + } + + let composite_time_ns = extract_sample(&mut cpu_frame_profiles, |a| a.composite_time_ns); + let paint_time_ns = extract_sample(&mut gpu_frame_profiles, |a| a.paint_time_ns); + let backend_time_ns = extract_sample(&mut cpu_frame_profiles, |a| a.backend_time_ns); + + TestProfile { + name: filename.to_str().unwrap().to_string(), + composite_time_ns, + paint_time_ns, + backend_time_ns, + draw_calls, + } + } +} + +// returns min, average, max, after removing the lowest and highest SAMPLE_EXCLUDE_COUNT +// samples (each). +fn extract_sample<F, T>(profiles: &mut [T], f: F) -> TestProfileRange +where + F: Fn(&T) -> u64, +{ + let mut samples: Vec<u64> = profiles.iter().map(f).collect(); + samples.sort_unstable(); + let useful_samples = &samples[SAMPLE_EXCLUDE_COUNT .. samples.len() - SAMPLE_EXCLUDE_COUNT]; + let total_time: u64 = useful_samples.iter().sum(); + TestProfileRange { + min: useful_samples[0], + avg: total_time / useful_samples.len() as u64, + max: useful_samples[useful_samples.len()-1] + } +} + +fn select_color(base: f32, value: f32) -> &'static str { + let tolerance = base * 0.1; + if (value - base).abs() < tolerance { + COLOR_DEFAULT + } else if value > base { + COLOR_RED + } else { + COLOR_GREEN + } +} + +pub fn compare(first_filename: &str, second_filename: &str) { + let profile0 = Profile::load(first_filename); + let profile1 = Profile::load(second_filename); + + let (set0, map0) = profile0.build_set_and_map_of_tests(); + let (set1, map1) = profile1.build_set_and_map_of_tests(); + + print!("+------------------------------------------------"); + println!("+--------------+------------------+------------------+"); + print!("| Test name "); + println!("| Draw Calls | Composite (ms) | Paint (ms) |"); + print!("+------------------------------------------------"); + println!("+--------------+------------------+------------------+"); + + for test_name in set0.symmetric_difference(&set1) { + println!( + "| {}{:47}{}|{:14}|{:18}|{:18}|", + COLOR_MAGENTA, + test_name, + COLOR_DEFAULT, + " -", + " -", + " -" + ); + } + + for test_name in set0.intersection(&set1) { + let test0 = &map0[test_name]; + let test1 = &map1[test_name]; + + let composite_time0 = test0.composite_time_ns.avg as f32 / 1000000.0; + let composite_time1 = test1.composite_time_ns.avg as f32 / 1000000.0; + + let paint_time0 = test0.paint_time_ns.avg as f32 / 1000000.0; + let paint_time1 = test1.paint_time_ns.avg as f32 / 1000000.0; + + let draw_calls_color = match test0.draw_calls.cmp(&test1.draw_calls) { + std::cmp::Ordering::Equal => COLOR_DEFAULT, + std::cmp::Ordering::Greater => COLOR_GREEN, + std::cmp::Ordering::Less => COLOR_RED, + }; + + let composite_time_color = select_color(composite_time0, composite_time1); + let paint_time_color = select_color(paint_time0, paint_time1); + + let draw_call_string = format!(" {} -> {}", test0.draw_calls, test1.draw_calls); + let composite_time_string = format!(" {:.2} -> {:.2}", composite_time0, composite_time1); + let paint_time_string = format!(" {:.2} -> {:.2}", paint_time0, paint_time1); + + println!( + "| {:47}|{}{:14}{}|{}{:18}{}|{}{:18}{}|", + test_name, + draw_calls_color, + draw_call_string, + COLOR_DEFAULT, + composite_time_color, + composite_time_string, + COLOR_DEFAULT, + paint_time_color, + paint_time_string, + COLOR_DEFAULT + ); + } + + print!("+------------------------------------------------"); + println!("+--------------+------------------+------------------+"); +} diff --git a/gfx/wr/wrench/src/png.rs b/gfx/wr/wrench/src/png.rs new file mode 100644 index 0000000000..6a7dfd94f8 --- /dev/null +++ b/gfx/wr/wrench/src/png.rs @@ -0,0 +1,118 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::{WindowWrapper, NotifierEvent}; +use image::png::PNGEncoder; +use image::{self, ColorType, GenericImageView}; +use std::fs::File; +use std::path::{Path, PathBuf}; +use std::sync::mpsc::Receiver; +use webrender::api::units::*; +use crate::wrench::{Wrench, WrenchThing}; +use crate::yaml_frame_reader::YamlFrameReader; + +pub enum ReadSurface { + Screen, + GpuCache, +} + +pub struct SaveSettings { + pub flip_vertical: bool, + pub try_crop: bool, +} + +pub fn save<P: Clone + AsRef<Path>>( + path: P, + orig_pixels: Vec<u8>, + size: DeviceIntSize, + settings: SaveSettings +) { + let mut width = size.width as u32; + let mut height = size.height as u32; + let mut buffer = image::RgbaImage::from_raw( + width, + height, + orig_pixels, + ).expect("bug: unable to construct image buffer"); + + if settings.flip_vertical { + // flip image vertically (texture is upside down) + buffer = image::imageops::flip_vertical(&buffer); + } + + if settings.try_crop { + if let Ok(existing_image) = image::open(path.clone()) { + let old_dims = existing_image.dimensions(); + println!("Crop from {:?} to {:?}", size, old_dims); + width = old_dims.0; + height = old_dims.1; + buffer = image::imageops::crop( + &mut buffer, + 0, + 0, + width, + height + ).to_image(); + } + } + + let encoder = PNGEncoder::new(File::create(path).unwrap()); + encoder + .encode(&buffer, width, height, ColorType::Rgba8) + .expect("Unable to encode PNG!"); +} + +pub fn save_flipped<P: Clone + AsRef<Path>>( + path: P, + orig_pixels: Vec<u8>, + size: DeviceIntSize, +) { + save(path, orig_pixels, size, SaveSettings { + flip_vertical: true, + try_crop: true, + }) +} + +pub fn png( + wrench: &mut Wrench, + surface: ReadSurface, + window: &mut WindowWrapper, + mut reader: YamlFrameReader, + rx: Receiver<NotifierEvent>, + out_path: Option<PathBuf>, +) { + reader.do_frame(wrench); + + // wait for the frame + rx.recv().unwrap(); + wrench.render(); + + let (fb_size, data, settings) = match surface { + ReadSurface::Screen => { + let dim = window.get_inner_size(); + let rect = FramebufferIntSize::new(dim.width, dim.height).into(); + let data = wrench.renderer.read_pixels_rgba8(rect); + (dim, data, SaveSettings { + flip_vertical: true, + try_crop: true, + }) + } + ReadSurface::GpuCache => { + let (size, data) = wrench.renderer + .read_gpu_cache(); + (size, data, SaveSettings { + flip_vertical: false, + try_crop: false, + }) + } + }; + + let out_path = out_path.unwrap_or_else(|| { + let mut path = reader.yaml_path().clone(); + path.set_extension("png"); + path + }); + + save(out_path, data, fb_size, settings); +} diff --git a/gfx/wr/wrench/src/premultiply.rs b/gfx/wr/wrench/src/premultiply.rs new file mode 100644 index 0000000000..04f43add56 --- /dev/null +++ b/gfx/wr/wrench/src/premultiply.rs @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// These are slow. Gecko's gfx/2d/Swizzle.cpp has better versions +pub fn premultiply(data: &mut [u8]) { + for pixel in data.chunks_mut(4) { + let a = pixel[3] as u32; + let b = pixel[2] as u32; + let g = pixel[1] as u32; + let r = pixel[0] as u32; + + pixel[3] = a as u8; + pixel[2] = ((r * a + 128) / 255) as u8; + pixel[1] = ((g * a + 128) / 255) as u8; + pixel[0] = ((b * a + 128) / 255) as u8; + } +} + +#[allow(unused)] +pub fn unpremultiply(data: &mut [u8]) { + for pixel in data.chunks_mut(4) { + let a = pixel[3] as u32; + let mut b = pixel[2] as u32; + let mut g = pixel[1] as u32; + let mut r = pixel[0] as u32; + + if a > 0 { + r = r * 255 / a; + g = g * 255 / a; + b = b * 255 / a; + } + + pixel[3] = a as u8; + pixel[2] = r as u8; + pixel[1] = g as u8; + pixel[0] = b as u8; + } +} + +#[test] +fn it_works() { + let mut f = [0xff, 0xff, 0xff, 0x80, 0x00, 0xff, 0x00, 0x80]; + premultiply(&mut f); + println!("{:?}", f); + assert!( + f[0] == 0x80 && f[1] == 0x80 && f[2] == 0x80 && f[3] == 0x80 && f[4] == 0x00 && + f[5] == 0x80 && f[6] == 0x00 && f[7] == 0x80 + ); + unpremultiply(&mut f); + println!("{:?}", f); + assert!( + f[0] == 0xff && f[1] == 0xff && f[2] == 0xff && f[3] == 0x80 && f[4] == 0x00 && + f[5] == 0xff && f[6] == 0x00 && f[7] == 0x80 + ); +} diff --git a/gfx/wr/wrench/src/rawtest.rs b/gfx/wr/wrench/src/rawtest.rs new file mode 100644 index 0000000000..19d3b025f7 --- /dev/null +++ b/gfx/wr/wrench/src/rawtest.rs @@ -0,0 +1,1450 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use euclid::{point2, size2, rect, Box2D}; +use std::sync::Arc; +use std::sync::atomic::{AtomicIsize, Ordering}; +use std::sync::mpsc::Receiver; +use webrender::api::*; +use webrender::render_api::*; +use webrender::api::units::*; +use crate::{WindowWrapper, NotifierEvent}; +use crate::blob; +use crate::reftest::{ReftestImage, ReftestImageComparison}; +use crate::wrench::Wrench; + +pub struct RawtestHarness<'a> { + wrench: &'a mut Wrench, + rx: &'a Receiver<NotifierEvent>, + window: &'a mut WindowWrapper, +} + + +impl<'a> RawtestHarness<'a> { + pub fn new(wrench: &'a mut Wrench, + window: &'a mut WindowWrapper, + rx: &'a Receiver<NotifierEvent>) -> Self { + RawtestHarness { + wrench, + rx, + window, + } + } + + pub fn run(mut self) { + self.test_hit_testing(); + self.test_resize_image(); + self.test_retained_blob_images_test(); + self.test_blob_update_test(); + self.test_blob_update_epoch_test(); + self.test_tile_decomposition(); + self.test_very_large_blob(); + self.test_blob_visible_area(); + self.test_blob_set_visible_area(); + self.test_offscreen_blob(); + self.test_save_restore(); + self.test_blur_cache(); + self.test_capture(); + self.test_zero_height_window(); + self.test_clear_cache(); + } + + fn render_and_get_pixels(&mut self, window_rect: FramebufferIntRect) -> Vec<u8> { + self.rx.recv().unwrap(); + self.wrench.render(); + self.wrench.renderer.read_pixels_rgba8(window_rect) + } + + fn compare_pixels(&self, data1: Vec<u8>, data2: Vec<u8>, size: FramebufferIntSize) { + let size = DeviceIntSize::new(size.width, size.height); + let image1 = ReftestImage { + data: data1, + size, + }; + let image2 = ReftestImage { + data: data2, + size, + }; + + match image1.compare(&image2) { + ReftestImageComparison::Equal => {} + ReftestImageComparison::NotEqual { max_difference, count_different, .. } => { + let t = "rawtest"; + println!( + "REFTEST TEST-UNEXPECTED-FAIL | {t} \ + | image comparison, max difference: {max_difference}, \ + number of differing pixels: {count_different}"); + println!("REFTEST IMAGE 1: {}", image1.create_data_uri()); + println!("REFTEST IMAGE 2: {}", image2.create_data_uri()); + println!("REFTEST TEST-END | {}", t); + panic!(); + } + } + } + + fn submit_dl( + &mut self, + epoch: &mut Epoch, + mut builder: DisplayListBuilder, + mut txn: Transaction, + ) { + txn.use_scene_builder_thread(); + + txn.set_display_list( + *epoch, + builder.end(), + ); + epoch.0 += 1; + + txn.generate_frame(0, RenderReasons::TESTING); + self.wrench.api.send_transaction(self.wrench.document_id, txn); + } + + fn make_common_properties(&self, clip_rect: LayoutRect) -> CommonItemProperties { + let space_and_clip = SpaceAndClipInfo::root_scroll(self.wrench.root_pipeline_id); + CommonItemProperties { + clip_rect, + clip_chain_id: space_and_clip.clip_chain_id, + spatial_id: space_and_clip.spatial_id, + flags: PrimitiveFlags::default(), + } + } + + fn make_common_properties_with_clip_and_spatial( + &self, + clip_rect: LayoutRect, + clip_chain_id: ClipChainId, + spatial_id: SpatialId + ) -> CommonItemProperties { + CommonItemProperties { + clip_rect, + clip_chain_id, + spatial_id, + flags: PrimitiveFlags::default(), + } + } + + fn test_resize_image(&mut self) { + println!("\tresize image..."); + // This test changes the size of an image to make it go switch back and forth + // between tiled and non-tiled. + // The resource cache should be able to handle this without crashing. + + let mut txn = Transaction::new(); + let img = self.wrench.api.generate_image_key(); + + // Start with a non-tiled image. + txn.add_image( + img, + ImageDescriptor::new(64, 64, ImageFormat::BGRA8, ImageDescriptorFlags::IS_OPAQUE), + ImageData::new(vec![255; 64 * 64 * 4]), + None, + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let info = self.make_common_properties(rect(0.0, 0.0, 64.0, 64.0).to_box2d()); + + builder.push_image( + &info, + info.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + img, + ColorF::WHITE, + ); + + let mut epoch = Epoch(0); + + self.submit_dl(&mut epoch, builder, txn); + self.rx.recv().unwrap(); + self.wrench.render(); + + let mut txn = Transaction::new(); + // Resize the image to something bigger than the max texture size (8196) to force tiling. + txn.update_image( + img, + ImageDescriptor::new(8200, 32, ImageFormat::BGRA8, ImageDescriptorFlags::IS_OPAQUE), + ImageData::new(vec![255; 8200 * 32 * 4]), + &DirtyRect::All, + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let info = self.make_common_properties(rect(0.0, 0.0, 1024.0, 1024.0).to_box2d()); + + builder.push_image( + &info, + info.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + img, + ColorF::WHITE, + ); + + self.submit_dl(&mut epoch, builder, txn); + self.rx.recv().unwrap(); + self.wrench.render(); + + let mut txn = Transaction::new(); + // Resize back to something doesn't require tiling. + txn.update_image( + img, + ImageDescriptor::new(64, 64, ImageFormat::BGRA8, ImageDescriptorFlags::IS_OPAQUE), + ImageData::new(vec![64; 64 * 64 * 4]), + &DirtyRect::All, + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let info = self.make_common_properties(rect(0.0, 0.0, 1024.0, 1024.0).to_box2d()); + + builder.push_image( + &info, + info.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + img, + ColorF::WHITE, + ); + + self.submit_dl(&mut epoch, builder, txn); + self.rx.recv().unwrap(); + self.wrench.render(); + + txn = Transaction::new(); + txn.delete_image(img); + self.wrench.api.send_transaction(self.wrench.document_id, txn); + } + + fn test_tile_decomposition(&mut self) { + println!("\ttile decomposition..."); + // This exposes a crash in tile decomposition + let mut txn = Transaction::new(); + + let blob_img = self.wrench.api.generate_blob_image_key(); + txn.add_blob_image( + blob_img, + ImageDescriptor::new(151, 56, ImageFormat::BGRA8, ImageDescriptorFlags::IS_OPAQUE), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect::from_size(DeviceIntSize::new(151, 56)), + Some(128), + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let info = self.make_common_properties(rect(448.9, 74.0, 151.000_03, 56.).to_box2d()); + + // setup some malicious image size parameters + builder.push_repeating_image( + &info, + info.clip_rect, + size2(151., 56.0), + size2(151.0, 56.0), + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + let mut epoch = Epoch(0); + + self.submit_dl(&mut epoch, builder, txn); + + self.rx.recv().unwrap(); + self.wrench.render(); + + // Leaving a tiled blob image in the resource cache + // confuses the `test_capture`. TODO: remove this + txn = Transaction::new(); + txn.delete_blob_image(blob_img); + self.wrench.api.send_transaction(self.wrench.document_id, txn); + } + + fn test_very_large_blob(&mut self) { + println!("\tvery large blob..."); + + let window_size = self.window.get_inner_size(); + + let test_size = FramebufferIntSize::new(800, 800); + + let window_rect = FramebufferIntRect::from_origin_and_size( + FramebufferIntPoint::new(0, window_size.height - test_size.height), + test_size, + ); + + // This exposes a crash in tile decomposition + let mut txn = Transaction::new(); + + let blob_img = self.wrench.api.generate_blob_image_key(); + txn.add_blob_image( + blob_img, + ImageDescriptor::new(15000, 15000, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect::from_size(DeviceIntSize::new(15000, 15000)), + Some(100), + ); + + let called = Arc::new(AtomicIsize::new(0)); + let called_inner = Arc::clone(&called); + + self.wrench.callbacks.lock().unwrap().request = Box::new(move |_| { + called_inner.fetch_add(1, Ordering::SeqCst); + }); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let root_space_and_clip = SpaceAndClipInfo::root_scroll(self.wrench.root_pipeline_id); + let clip_id = builder.define_clip_rect( + root_space_and_clip.spatial_id, + rect(40., 41., 200., 201.).to_box2d(), + ); + let clip_chain_id = builder.define_clip_chain(None, [clip_id]); + + let info = CommonItemProperties { + clip_rect: rect(0.0, 0.0, 800.0, 800.0).to_box2d(), + clip_chain_id, + spatial_id: root_space_and_clip.spatial_id, + flags: PrimitiveFlags::default(), + }; + + // setup some malicious image size parameters + builder.push_repeating_image( + &info, + size2(15000.0, 15000.0).into(), + size2(15000.0, 15000.0), + size2(0.0, 0.0), + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + let mut epoch = Epoch(0); + + self.submit_dl(&mut epoch, builder, txn); + + let pixels = self.render_and_get_pixels(window_rect); + + // make sure we didn't request too many blobs + assert!(called.load(Ordering::SeqCst) < 20); + + //use crate::png; + //png::save_flipped("out.png", pixels.clone(), size2(window_rect.size.width, window_rect.size.height)); + + // make sure things are in the right spot + let w = window_rect.width() as usize; + let h = window_rect.height() as usize; + let p1 = (40 + (h - 100) * w) * 4; + assert_eq!(pixels[p1 ], 50); + assert_eq!(pixels[p1 + 1], 50); + assert_eq!(pixels[p1 + 2], 150); + assert_eq!(pixels[p1 + 3], 255); + + // Leaving a tiled blob image in the resource cache + // confuses the `test_capture`. TODO: remove this + txn = Transaction::new(); + txn.delete_blob_image(blob_img); + self.wrench.api.send_transaction(self.wrench.document_id, txn); + + *self.wrench.callbacks.lock().unwrap() = blob::BlobCallbacks::new(); + } + + fn test_blob_visible_area(&mut self) { + println!("\tblob visible area..."); + + let window_size = self.window.get_inner_size(); + let test_size = FramebufferIntSize::new(800, 800); + let window_rect = FramebufferIntRect::from_origin_and_size( + FramebufferIntPoint::new(0, window_size.height - test_size.height), + test_size, + ); + let mut txn = Transaction::new(); + + let blob_img = self.wrench.api.generate_blob_image_key(); + txn.add_blob_image( + blob_img, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect { + min: point2(50, 20), + max: point2(450, 420), + }, + Some(100), + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let image_size = size2(400.0, 400.0); + + let root_space_and_clip = SpaceAndClipInfo::root_scroll(self.wrench.root_pipeline_id); + let clip_id = builder.define_clip_rect( + root_space_and_clip.spatial_id, + rect(-1000.0, -1000.0, 2000.0, 2000.0).to_box2d(), + ); + let clip_chain_id = builder.define_clip_chain(None, [clip_id]); + + let info = CommonItemProperties { + clip_rect: rect(10.0, 10.0, 400.0, 400.0).to_box2d(), + clip_chain_id, + spatial_id: root_space_and_clip.spatial_id, + flags: PrimitiveFlags::default(), + }; + + builder.push_repeating_image( + &info, + info.clip_rect, + image_size, + image_size, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + let mut epoch = Epoch(0); + + self.submit_dl(&mut epoch, builder, txn); + + let pixels = self.render_and_get_pixels(window_rect); + + //use super::png; + //png::save_flipped("out.png", pixels.clone(), size2(window_rect.size.width, window_rect.size.height)); + + + // make sure things are in the right spot + let w = window_rect.width() as usize; + let h = window_rect.height() as usize; + let p1 = (65 + (h - 15) * w) * 4; + assert_eq!(pixels[p1 ], 255); + assert_eq!(pixels[p1 + 1], 255); + assert_eq!(pixels[p1 + 2], 255); + assert_eq!(pixels[p1 + 3], 255); + + let p2 = (25 + (h - 15) * w) * 4; + assert_eq!(pixels[p2 ], 221); + assert_eq!(pixels[p2 + 1], 221); + assert_eq!(pixels[p2 + 2], 221); + assert_eq!(pixels[p2 + 3], 255); + + let p3 = (15 + (h - 15) * w) * 4; + assert_eq!(pixels[p3 ], 50); + assert_eq!(pixels[p3 + 1], 50); + assert_eq!(pixels[p3 + 2], 150); + assert_eq!(pixels[p3 + 3], 255); + + // Leaving a tiled blob image in the resource cache + // confuses the `test_capture`. TODO: remove this + txn = Transaction::new(); + txn.delete_blob_image(blob_img); + self.wrench.api.send_transaction(self.wrench.document_id, txn); + + *self.wrench.callbacks.lock().unwrap() = blob::BlobCallbacks::new(); + } + + fn test_blob_set_visible_area(&mut self) { + // In this test we first render a blob with a certain visible area, + // then change the visible area without updating the blob image. + + println!("\tblob visible area update..."); + + let window_size = self.window.get_inner_size(); + let test_size = FramebufferIntSize::new(800, 800); + let window_rect = FramebufferIntRect::from_origin_and_size( + FramebufferIntPoint::new(0, window_size.height - test_size.height), + test_size, + ); + let mut txn = Transaction::new(); + + let blob_img = self.wrench.api.generate_blob_image_key(); + txn.add_blob_image( + blob_img, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect { + min: point2(0, 0), + max: point2(500, 500), + }, + Some(128), + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let root_space_and_clip = SpaceAndClipInfo::root_scroll(self.wrench.root_pipeline_id); + let clip_id = builder.define_clip_rect( + root_space_and_clip.spatial_id, + rect(-1000.0, -1000.0, 2000.0, 2000.0).to_box2d(), + ); + let clip_chain_id = builder.define_clip_chain(None, [clip_id]); + + let info = CommonItemProperties { + clip_rect: rect(0.0, 0.0, 1000.0, 1000.0).to_box2d(), + clip_chain_id, + spatial_id: root_space_and_clip.spatial_id, + flags: PrimitiveFlags::default(), + }; + + builder.push_repeating_image( + &info, + rect(0.0, 0.0, 500.0, 500.0).to_box2d(), + size2(500.0, 500.0), + size2(500.0, 500.0), + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + let mut epoch = Epoch(0); + + // Render the first display list. We don't care about the result but we + // want to make sure the next display list updates an already rendered + // state. + self.submit_dl(&mut epoch, builder, txn); + let _ = self.render_and_get_pixels(window_rect); + + // Now render a similar scene with an updated blob visible area. + // In this test we care about the fact that the visible area was updated + // without using update_blob_image. + + let mut txn = Transaction::new(); + + txn.set_blob_image_visible_area(blob_img, DeviceIntRect { + min: point2(50, 50), + max: point2(450, 450), + }); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let root_space_and_clip = SpaceAndClipInfo::root_scroll(self.wrench.root_pipeline_id); + let clip_id = builder.define_clip_rect( + root_space_and_clip.spatial_id, + rect(-1000.0, -1000.0, 2000.0, 2000.0).to_box2d(), + ); + let clip_chain_id = builder.define_clip_chain(None, [clip_id]); + + let info = CommonItemProperties { + clip_rect: rect(0.0, 0.0, 1000.0, 1000.0).to_box2d(), + clip_chain_id, + spatial_id: root_space_and_clip.spatial_id, + flags: PrimitiveFlags::default(), + }; + + builder.push_repeating_image( + &info, + rect(50.0, 50.0, 400.0, 400.0).to_box2d(), + size2(400.0, 400.0), + size2(400.0, 400.0), + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + self.submit_dl(&mut epoch, builder, txn); + let resized_pixels = self.render_and_get_pixels(window_rect); + + // Now render the same scene with a new blob image created with the same + // visible area as the previous scene, without going through an update. + + let mut txn = Transaction::new(); + + let blob_img2 = self.wrench.api.generate_blob_image_key(); + txn.add_blob_image( + blob_img2, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect { + min: point2(50, 50), + max: point2(450, 450), + }, + Some(128), + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let root_space_and_clip = SpaceAndClipInfo::root_scroll(self.wrench.root_pipeline_id); + let clip_id = builder.define_clip_rect( + root_space_and_clip.spatial_id, + rect(-1000.0, -1000.0, 2000.0, 2000.0).to_box2d(), + ); + let clip_chain_id = builder.define_clip_chain(None, [clip_id]); + + let info = CommonItemProperties { + clip_rect: rect(0.0, 0.0, 1000.0, 1000.0).to_box2d(), + clip_chain_id, + spatial_id: root_space_and_clip.spatial_id, + flags: PrimitiveFlags::default(), + }; + + builder.push_repeating_image( + &info, + rect(50.0, 50.0, 400.0, 400.0).to_box2d(), + size2(400.0, 400.0), + size2(400.0, 400.0), + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img2.as_image(), + ColorF::WHITE, + ); + let mut epoch = Epoch(0); + + self.submit_dl(&mut epoch, builder, txn); + + let reference_pixels = self.render_and_get_pixels(window_rect); + + assert_eq!(resized_pixels, reference_pixels); + + txn = Transaction::new(); + txn.delete_blob_image(blob_img); + txn.delete_blob_image(blob_img2); + self.wrench.api.send_transaction(self.wrench.document_id, txn); + } + + fn test_offscreen_blob(&mut self) { + println!("\toffscreen blob update..."); + + let window_size = self.window.get_inner_size(); + + let test_size = FramebufferIntSize::new(800, 800); + let window_rect = FramebufferIntRect::from_origin_and_size( + point2(0, window_size.height - test_size.height), + test_size, + ); + + // This exposes a crash in tile decomposition + let mut txn = Transaction::new(); + + let blob_img = self.wrench.api.generate_blob_image_key(); + txn.add_blob_image( + blob_img, + ImageDescriptor::new(1510, 1510, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect::from_size(size2(1510, 1510)), + None, + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let info = self.make_common_properties(rect(0., 0.0, 1510., 1510.).to_box2d()); + + let image_size = size2(1510., 1510.); + + // setup some malicious image size parameters + builder.push_repeating_image( + &info, + info.clip_rect, + image_size, + image_size, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + let mut epoch = Epoch(0); + + self.submit_dl(&mut epoch, builder, txn); + + let original_pixels = self.render_and_get_pixels(window_rect); + + let mut epoch = Epoch(1); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let info = self.make_common_properties(rect(-10000., 0.0, 1510., 1510.).to_box2d()); + + let image_size = size2(1510., 1510.); + + // setup some malicious image size parameters + builder.push_repeating_image( + &info, + info.clip_rect, + image_size, + image_size, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + self.submit_dl(&mut epoch, builder, Transaction::new()); + + let _offscreen_pixels = self.render_and_get_pixels(window_rect); + + let mut txn = Transaction::new(); + + txn.update_blob_image( + blob_img, + ImageDescriptor::new(1510, 1510, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect::from_size(size2(1510, 1510)), + &Box2D { min: point2(10, 10), max: point2(110, 110) }.into(), + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let info = self.make_common_properties(rect(0., 0.0, 1510., 1510.).to_box2d()); + + let image_size = size2(1510., 1510.); + + // setup some malicious image size parameters + builder.push_repeating_image( + &info, + info.clip_rect, + image_size, + image_size, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + let mut epoch = Epoch(2); + + self.submit_dl(&mut epoch, builder, txn); + + let pixels = self.render_and_get_pixels(window_rect); + + self.compare_pixels(original_pixels, pixels, window_rect.size()); + + // Leaving a tiled blob image in the resource cache + // confuses the `test_capture`. TODO: remove this + txn = Transaction::new(); + txn.delete_blob_image(blob_img); + self.wrench.api.send_transaction(self.wrench.document_id, txn); + } + + fn test_retained_blob_images_test(&mut self) { + println!("\tretained blob images test..."); + let blob_img; + let window_size = self.window.get_inner_size(); + + let test_size = FramebufferIntSize::new(400, 400); + let window_rect = FramebufferIntRect::from_origin_and_size( + FramebufferIntPoint::new(0, window_size.height - test_size.height), + test_size, + ); + + let mut txn = Transaction::new(); + { + let api = &self.wrench.api; + + blob_img = api.generate_blob_image_key(); + txn.add_blob_image( + blob_img, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect::from_size(size2(500, 500)), + None, + ); + } + + let called = Arc::new(AtomicIsize::new(0)); + let called_inner = Arc::clone(&called); + + self.wrench.callbacks.lock().unwrap().request = Box::new(move |_| { + assert_eq!(0, called_inner.fetch_add(1, Ordering::SeqCst)); + }); + + // draw the blob the first time + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let info = self.make_common_properties(rect(0.0, 60.0, 200.0, 200.0).to_box2d()); + + builder.push_image( + &info, + info.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + let mut epoch = Epoch(0); + + self.submit_dl(&mut epoch, builder, txn); + + let pixels_first = self.render_and_get_pixels(window_rect); + + assert_eq!(1, called.load(Ordering::SeqCst)); + + // draw the blob image a second time at a different location + + // make a new display list that refers to the first image + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let info = self.make_common_properties(rect(1.0, 60.0, 200.0, 200.0).to_box2d()); + builder.push_image( + &info, + info.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + let mut txn = Transaction::new(); + txn.resource_updates.clear(); + + self.submit_dl(&mut epoch, builder, txn); + + let pixels_second = self.render_and_get_pixels(window_rect); + + // make sure we only requested once + assert_eq!(1, called.load(Ordering::SeqCst)); + + // use png; + // png::save_flipped("out1.png", &pixels_first, window_rect.size); + // png::save_flipped("out2.png", &pixels_second, window_rect.size); + assert!(pixels_first != pixels_second); + + // cleanup + *self.wrench.callbacks.lock().unwrap() = blob::BlobCallbacks::new(); + } + + fn test_blob_update_epoch_test(&mut self) { + println!("\tblob update epoch test..."); + let (blob_img, blob_img2); + let window_size = self.window.get_inner_size(); + + let test_size = FramebufferIntSize::new(400, 400); + let window_rect = FramebufferIntRect::from_origin_and_size( + point2(0, window_size.height - test_size.height), + test_size, + ); + + let mut txn = Transaction::new(); + let (blob_img, blob_img2) = { + let api = &self.wrench.api; + + blob_img = api.generate_blob_image_key(); + txn.add_blob_image( + blob_img, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect::from_size(size2(500, 500)), + None, + ); + blob_img2 = api.generate_blob_image_key(); + txn.add_blob_image( + blob_img2, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(80, 50, 150, 255)), + DeviceIntRect::from_size(size2(500, 500)), + None, + ); + (blob_img, blob_img2) + }; + + // setup some counters to count how many times each image is requested + let img1_requested = Arc::new(AtomicIsize::new(0)); + let img1_requested_inner = Arc::clone(&img1_requested); + let img2_requested = Arc::new(AtomicIsize::new(0)); + let img2_requested_inner = Arc::clone(&img2_requested); + + // track the number of times that the second image has been requested + self.wrench.callbacks.lock().unwrap().request = Box::new(move |requests| { + for item in requests { + if item.request.key == blob_img { + img1_requested_inner.fetch_add(1, Ordering::SeqCst); + } + if item.request.key == blob_img2 { + img2_requested_inner.fetch_add(1, Ordering::SeqCst); + } + } + }); + + // create two blob images and draw them + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let info = self.make_common_properties(rect(0.0, 60.0, 200.0, 200.0).to_box2d()); + let info2 = self.make_common_properties(rect(200.0, 60.0, 200.0, 200.0).to_box2d()); + let push_images = |builder: &mut DisplayListBuilder| { + builder.push_image( + &info, + info.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + builder.push_image( + &info2, + info2.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img2.as_image(), + ColorF::WHITE, + ); + }; + + push_images(&mut builder); + + let mut epoch = Epoch(0); + + self.submit_dl(&mut epoch, builder, txn); + let _pixels_first = self.render_and_get_pixels(window_rect); + + // update and redraw both images + let mut txn = Transaction::new(); + txn.update_blob_image( + blob_img, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect::from_size(size2(500, 500)), + &Box2D { min: point2(100, 100), max: point2(200, 200) }.into(), + ); + txn.update_blob_image( + blob_img2, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(59, 50, 150, 255)), + DeviceIntRect::from_size(size2(500, 500)), + &Box2D { min: point2(100, 100), max: point2(200, 200) }.into(), + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + push_images(&mut builder); + self.submit_dl(&mut epoch, builder, txn); + let _pixels_second = self.render_and_get_pixels(window_rect); + + // only update the first image + let mut txn = Transaction::new(); + txn.update_blob_image( + blob_img, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 150, 150, 255)), + DeviceIntRect::from_size(size2(500, 500)), + &Box2D { min: point2(200, 200), max: point2(300, 300) }.into(), + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + push_images(&mut builder); + self.submit_dl(&mut epoch, builder, txn); + let _pixels_third = self.render_and_get_pixels(window_rect); + + // the first image should be requested 3 times + assert_eq!(img1_requested.load(Ordering::SeqCst), 3); + // the second image should've been requested twice + assert_eq!(img2_requested.load(Ordering::SeqCst), 2); + + // cleanup + *self.wrench.callbacks.lock().unwrap() = blob::BlobCallbacks::new(); + } + + fn test_blob_update_test(&mut self) { + println!("\tblob update test..."); + let window_size = self.window.get_inner_size(); + + let test_size = FramebufferIntSize::new(400, 400); + let window_rect = FramebufferIntRect::from_origin_and_size( + point2(0, window_size.height - test_size.height), + test_size, + ); + let mut txn = Transaction::new(); + + let blob_img = { + let img = self.wrench.api.generate_blob_image_key(); + txn.add_blob_image( + img, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect::from_size(size2(500, 500)), + None, + ); + img + }; + + // draw the blobs the first time + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let info = self.make_common_properties(rect(0.0, 60.0, 200.0, 200.0).to_box2d()); + + builder.push_image( + &info, + info.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + let mut epoch = Epoch(0); + + self.submit_dl(&mut epoch, builder, txn); + let pixels_first = self.render_and_get_pixels(window_rect); + + // draw the blob image a second time after updating it with the same color + let mut txn = Transaction::new(); + txn.update_blob_image( + blob_img, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 50, 150, 255)), + DeviceIntRect::from_size(size2(500, 500)), + &Box2D { min: point2(100, 100), max: point2(200, 200) }.into(), + ); + + // make a new display list that refers to the first image + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let info = self.make_common_properties(rect(0.0, 60.0, 200.0, 200.0).to_box2d()); + builder.push_image( + &info, + info.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + self.submit_dl(&mut epoch, builder, txn); + let pixels_second = self.render_and_get_pixels(window_rect); + + // draw the blob image a third time after updating it with a different color + let mut txn = Transaction::new(); + txn.update_blob_image( + blob_img, + ImageDescriptor::new(500, 500, ImageFormat::BGRA8, ImageDescriptorFlags::empty()), + blob::serialize_blob(ColorU::new(50, 150, 150, 255)), + DeviceIntRect::from_size(size2(500, 500)), + &Box2D { min: point2(200, 200), max: point2(300, 300) }.into(), + ); + + // make a new display list that refers to the first image + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let info = self.make_common_properties(rect(0.0, 60.0, 200.0, 200.0).to_box2d()); + builder.push_image( + &info, + info.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + blob_img.as_image(), + ColorF::WHITE, + ); + + self.submit_dl(&mut epoch, builder, txn); + let pixels_third = self.render_and_get_pixels(window_rect); + + assert!(pixels_first != pixels_third); + self.compare_pixels(pixels_first, pixels_second, window_rect.size()); + } + + // Ensures that content doing a save-restore produces the same results as not + fn test_save_restore(&mut self) { + println!("\tsave/restore..."); + let window_size = self.window.get_inner_size(); + + let test_size = FramebufferIntSize::new(400, 400); + let window_rect = FramebufferIntRect::from_origin_and_size( + point2(0, window_size.height - test_size.height), + test_size, + ); + + let mut do_test = |should_try_and_fail| { + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let spatial_id = SpatialId::root_scroll_node(self.wrench.root_pipeline_id); + let clip_id = builder.define_clip_rect( + SpatialId::root_scroll_node(self.wrench.root_pipeline_id), + rect(110., 120., 200., 200.).to_box2d(), + ); + let clip_chain_id = builder.define_clip_chain(None, [clip_id]); + builder.push_rect( + &self.make_common_properties_with_clip_and_spatial( + rect(100., 100., 100., 100.).to_box2d(), + clip_chain_id, + spatial_id), + rect(100., 100., 100., 100.).to_box2d(), + ColorF::new(0.0, 0.0, 1.0, 1.0), + ); + + if should_try_and_fail { + builder.save(); + let clip_id = builder.define_clip_rect( + spatial_id, + rect(80., 80., 90., 90.).to_box2d(), + ); + let clip_chain_id = builder.define_clip_chain(None, [clip_id]); + let space_and_clip = SpaceAndClipInfo { + spatial_id, + clip_chain_id, + }; + builder.push_rect( + &self.make_common_properties_with_clip_and_spatial( + rect(110., 110., 50., 50.).to_box2d(), + clip_chain_id, + spatial_id), + rect(110., 110., 50., 50.).to_box2d(), + ColorF::new(0.0, 1.0, 0.0, 1.0), + ); + builder.push_shadow( + &space_and_clip, + Shadow { + offset: LayoutVector2D::new(1.0, 1.0), + blur_radius: 1.0, + color: ColorF::new(0.0, 0.0, 0.0, 1.0), + }, + true, + ); + let info = CommonItemProperties { + clip_rect: rect(110., 110., 50., 2.).to_box2d(), + clip_chain_id, + spatial_id, + flags: PrimitiveFlags::default(), + }; + builder.push_line( + &info, + &info.clip_rect, + 0.0, LineOrientation::Horizontal, + &ColorF::new(0.0, 0.0, 0.0, 1.0), + LineStyle::Solid, + ); + builder.restore(); + } + + { + builder.save(); + let clip_id = builder.define_clip_rect( + spatial_id, + rect(80., 80., 100., 100.).to_box2d(), + ); + let clip_chain_id = builder.define_clip_chain(None, [clip_id]); + builder.push_rect( + &self.make_common_properties_with_clip_and_spatial( + rect(150., 150., 100., 100.).to_box2d(), + clip_chain_id, + spatial_id), + rect(150., 150., 100., 100.).to_box2d(), + ColorF::new(0.0, 0.0, 1.0, 1.0), + ); + builder.clear_save(); + } + + let txn = Transaction::new(); + + self.submit_dl(&mut Epoch(0), builder, txn); + + self.render_and_get_pixels(window_rect) + }; + + let first = do_test(false); + let second = do_test(true); + + self.compare_pixels(first, second, window_rect.size()); + } + + // regression test for #2769 + // "async scene building: cache collisions from reused picture ids" + fn test_blur_cache(&mut self) { + println!("\tblur cache..."); + let window_size = self.window.get_inner_size(); + + let test_size = FramebufferIntSize::new(400, 400); + let window_rect = FramebufferIntRect::from_origin_and_size( + point2(0, window_size.height - test_size.height), + test_size, + ); + + let mut do_test = |shadow_is_red| { + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let shadow_color = if shadow_is_red { + ColorF::new(1.0, 0.0, 0.0, 1.0) + } else { + ColorF::new(0.0, 1.0, 0.0, 1.0) + }; + + builder.push_shadow( + &SpaceAndClipInfo::root_scroll(self.wrench.root_pipeline_id), + Shadow { + offset: LayoutVector2D::new(1.0, 1.0), + blur_radius: 1.0, + color: shadow_color, + }, + true, + ); + let info = self.make_common_properties(rect(110., 110., 50., 2.).to_box2d()); + builder.push_line( + &info, + &info.clip_rect, + 0.0, LineOrientation::Horizontal, + &ColorF::new(0.0, 0.0, 0.0, 1.0), + LineStyle::Solid, + ); + builder.pop_all_shadows(); + + let txn = Transaction::new(); + self.submit_dl(&mut Epoch(0), builder, txn); + + self.render_and_get_pixels(window_rect) + }; + + let first = do_test(false); + let second = do_test(true); + + assert_ne!(first, second); + } + + fn test_capture(&mut self) { + println!("\tcapture..."); + let path = "../captures/test"; + let layout_size = LayoutSize::new(400., 400.); + let dim = self.window.get_inner_size(); + let window_rect = FramebufferIntRect::from_origin_and_size( + point2(0, dim.height - layout_size.height as i32), + size2(layout_size.width as i32, layout_size.height as i32), + ); + + // 1. render some scene + + let mut txn = Transaction::new(); + let image = self.wrench.api.generate_image_key(); + txn.add_image( + image, + ImageDescriptor::new(1, 1, ImageFormat::BGRA8, ImageDescriptorFlags::IS_OPAQUE), + ImageData::new(vec![0xFF, 0, 0, 0xFF]), + None, + ); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let info = self.make_common_properties(rect(300.0, 70.0, 150.0, 50.0).to_box2d()); + builder.push_image( + &info, + info.clip_rect, + ImageRendering::Auto, + AlphaType::PremultipliedAlpha, + image, + ColorF::WHITE, + ); + + let mut txn = Transaction::new(); + + txn.set_display_list( + Epoch(0), + builder.end(), + ); + txn.generate_frame(0, RenderReasons::TESTING); + + self.wrench.api.send_transaction(self.wrench.document_id, txn); + + let pixels0 = self.render_and_get_pixels(window_rect); + + // 2. capture it + self.wrench.api.save_capture(path.into(), CaptureBits::all()); + + // 3. set a different scene + + builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let mut txn = Transaction::new(); + txn.set_display_list( + Epoch(1), + builder.end(), + ); + self.wrench.api.send_transaction(self.wrench.document_id, txn); + + // 4. load the first one + + let mut documents = self.wrench.api.load_capture(path.into(), None); + let captured = documents.swap_remove(0); + + // 5. render the built frame and compare + let pixels1 = self.render_and_get_pixels(window_rect); + self.compare_pixels(pixels0.clone(), pixels1, window_rect.size()); + + // 6. rebuild the scene and compare again + let mut txn = Transaction::new(); + txn.set_root_pipeline(captured.root_pipeline_id.unwrap()); + txn.generate_frame(0, RenderReasons::TESTING); + self.wrench.api.send_transaction(captured.document_id, txn); + let pixels2 = self.render_and_get_pixels(window_rect); + self.compare_pixels(pixels0, pixels2, window_rect.size()); + } + + fn test_zero_height_window(&mut self) { + println!("\tzero height test..."); + + let layout_size = LayoutSize::new(120.0, 0.0); + let window_size = DeviceIntSize::new(layout_size.width as i32, layout_size.height as i32); + let doc_id = self.wrench.api.add_document(window_size); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + let info = self.make_common_properties( + LayoutRect::from_size(LayoutSize::new(100.0, 100.0)) + ); + builder.push_rect( + &info, + info.clip_rect, + ColorF::new(0.0, 1.0, 0.0, 1.0), + ); + + let mut txn = Transaction::new(); + txn.set_root_pipeline(self.wrench.root_pipeline_id); + txn.set_display_list( + Epoch(1), + builder.end(), + ); + txn.generate_frame(0, RenderReasons::TESTING); + self.wrench.api.send_transaction(doc_id, txn); + + // Ensure we get a notification from rendering the above, even though + // there are zero visible pixels + assert!(self.rx.recv().unwrap() == NotifierEvent::WakeUp { composite_needed: true }); + } + + + fn test_hit_testing(&mut self) { + println!("\thit testing test..."); + + let layout_size = LayoutSize::new(400., 400.); + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + // Add a rectangle that covers the entire scene. + let space_and_clip = SpaceAndClipInfo::root_scroll(self.wrench.root_pipeline_id); + builder.push_hit_test( + LayoutRect::from_size(layout_size), + ClipChainId::INVALID, + space_and_clip.spatial_id, + PrimitiveFlags::default(), + (0, 1), + ); + + // Add a simple 100x100 rectangle at 100,0. + builder.push_hit_test( + LayoutRect::from_origin_and_size( + LayoutPoint::new(100., 0.), + LayoutSize::new(100., 100.) + ), + ClipChainId::INVALID, + space_and_clip.spatial_id, + PrimitiveFlags::default(), + (0, 2), + ); + + let make_rounded_complex_clip = |rect: &LayoutRect, radius: f32| -> ComplexClipRegion { + ComplexClipRegion::new( + *rect, + BorderRadius::uniform_size(LayoutSize::new(radius, radius)), + ClipMode::Clip + ) + }; + + // Add a rectangle that is clipped by a rounded rect clip item. + let rect = LayoutRect::from_origin_and_size(LayoutPoint::new(100., 100.), LayoutSize::new(100., 100.)); + let temp_clip_id = builder.define_clip_rounded_rect( + space_and_clip.spatial_id, + make_rounded_complex_clip(&rect, 20.), + ); + let clip_chain_id = builder.define_clip_chain(None, vec![temp_clip_id]); + builder.push_hit_test( + rect, + clip_chain_id, + space_and_clip.spatial_id, + PrimitiveFlags::default(), + (0, 4), + ); + + // Add a rectangle that is clipped by a ClipChain containing a rounded rect. + let rect = LayoutRect::from_origin_and_size(LayoutPoint::new(200., 100.), LayoutSize::new(100., 100.)); + let clip_id = builder.define_clip_rounded_rect( + space_and_clip.spatial_id, + make_rounded_complex_clip(&rect, 20.), + ); + let clip_chain_id = builder.define_clip_chain(None, vec![clip_id]); + builder.push_hit_test( + rect, + clip_chain_id, + space_and_clip.spatial_id, + PrimitiveFlags::default(), + (0, 5), + ); + + let mut epoch = Epoch(0); + let txn = Transaction::new(); + self.submit_dl(&mut epoch, builder, txn); + + // We render to ensure that the hit tester is up to date with the current scene. + self.rx.recv().unwrap(); + self.wrench.render(); + + let hit_test = |point: WorldPoint| -> HitTestResult { + self.wrench.api.hit_test( + self.wrench.document_id, + point, + ) + }; + + let assert_hit_test = |point: WorldPoint, tags: Vec<ItemTag>| { + let result = hit_test(point); + assert_eq!(result.items.len(), tags.len()); + + for (hit_test_item, item_b) in result.items.iter().zip(tags.iter()) { + assert_eq!(hit_test_item.tag, *item_b); + } + }; + + // We should not have any hits outside the boundaries of the scene. + assert_hit_test(WorldPoint::new(-10., -10.), Vec::new()); + assert_hit_test(WorldPoint::new(-10., 10.), Vec::new()); + assert_hit_test(WorldPoint::new(450., 450.), Vec::new()); + assert_hit_test(WorldPoint::new(100., 450.), Vec::new()); + + // The top left corner of the scene should only contain the background. + assert_hit_test(WorldPoint::new(50., 50.), vec![(0, 1)]); + + // The middle of the normal rectangle should be hit. + assert_hit_test(WorldPoint::new(150., 50.), vec![(0, 2), (0, 1)]); + + let test_rounded_rectangle = |point: WorldPoint, size: WorldSize, tag: ItemTag| { + // The cut out corners of the rounded rectangle should not be hit. + let top_left = point + WorldVector2D::new(5., 5.); + let bottom_right = point + size.to_vector() - WorldVector2D::new(5., 5.); + + assert_hit_test( + WorldPoint::new(point.x + (size.width / 2.), point.y + (size.height / 2.)), + vec![tag, (0, 1)] + ); + + assert_hit_test(top_left, vec![(0, 1)]); + assert_hit_test(WorldPoint::new(bottom_right.x, top_left.y), vec![(0, 1)]); + assert_hit_test(WorldPoint::new(top_left.x, bottom_right.y), vec![(0, 1)]); + assert_hit_test(bottom_right, vec![(0, 1)]); + }; + + test_rounded_rectangle(WorldPoint::new(100., 100.), WorldSize::new(100., 100.), (0, 4)); + test_rounded_rectangle(WorldPoint::new(200., 100.), WorldSize::new(100., 100.), (0, 5)); + } + + fn test_clear_cache(&mut self) { + println!("\tclear cache test..."); + + self.wrench.api.send_message(ApiMsg::DebugCommand(DebugCommand::ClearCaches(ClearCache::all()))); + + let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); + builder.begin(); + + let txn = Transaction::new(); + let mut epoch = Epoch(0); + self.submit_dl(&mut epoch, builder, txn); + + self.rx.recv().unwrap(); + self.wrench.render(); + } +} diff --git a/gfx/wr/wrench/src/reftest.rs b/gfx/wr/wrench/src/reftest.rs new file mode 100644 index 0000000000..136a447720 --- /dev/null +++ b/gfx/wr/wrench/src/reftest.rs @@ -0,0 +1,970 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::{WindowWrapper, NotifierEvent}; +use image::load as load_piston_image; +use image::png::PNGEncoder; +use image::{ColorType, ImageFormat}; +use crate::parse_function::parse_function; +use crate::png::save_flipped; +use std::{cmp, env}; +use std::fmt::{Display, Error, Formatter}; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::mpsc::Receiver; +use webrender::RenderResults; +use webrender::api::*; +use webrender::render_api::*; +use webrender::api::units::*; +use crate::wrench::{Wrench, WrenchThing}; +use crate::yaml_frame_reader::YamlFrameReader; + + +const OPTION_DISABLE_SUBPX: &str = "disable-subpixel"; +const OPTION_DISABLE_AA: &str = "disable-aa"; +const OPTION_ALLOW_MIPMAPS: &str = "allow-mipmaps"; + +pub struct ReftestOptions { + // These override values that are lower. + pub allow_max_difference: usize, + pub allow_num_differences: usize, +} + +impl ReftestOptions { + pub fn default() -> Self { + ReftestOptions { + allow_max_difference: 0, + allow_num_differences: 0, + } + } +} + +#[derive(Debug, Copy, Clone)] +pub enum ReftestOp { + /// Expect that the images match the reference + Equal, + /// Expect that the images *don't* match the reference + NotEqual, + /// Expect that drawing the reference at different tiles sizes gives the same pixel exact result. + Accurate, + /// Expect that drawing the reference at different tiles sizes gives a *different* pixel exact result. + Inaccurate, +} + +impl Display for ReftestOp { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!( + f, + "{}", + match *self { + ReftestOp::Equal => "==".to_owned(), + ReftestOp::NotEqual => "!=".to_owned(), + ReftestOp::Accurate => "**".to_owned(), + ReftestOp::Inaccurate => "!*".to_owned(), + } + ) + } +} + +#[derive(Debug)] +enum ExtraCheck { + DrawCalls(usize), + AlphaTargets(usize), + ColorTargets(usize), +} + +impl ExtraCheck { + fn run(&self, results: &[RenderResults]) -> bool { + match *self { + ExtraCheck::DrawCalls(x) => + x == results.last().unwrap().stats.total_draw_calls, + ExtraCheck::AlphaTargets(x) => + x == results.last().unwrap().stats.alpha_target_count, + ExtraCheck::ColorTargets(x) => + x == results.last().unwrap().stats.color_target_count, + } + } +} + +pub struct RefTestFuzzy { + max_difference: usize, + num_differences: usize, +} + +pub struct Reftest { + op: ReftestOp, + test: Vec<PathBuf>, + reference: PathBuf, + font_render_mode: Option<FontRenderMode>, + fuzziness: Vec<RefTestFuzzy>, + extra_checks: Vec<ExtraCheck>, + allow_mipmaps: bool, + force_subpixel_aa_where_possible: Option<bool>, + max_surface_override: Option<usize>, +} + +impl Reftest { + /// Check the positive case (expecting equality) and report details if different + fn check_and_report_equality_failure( + &self, + comparison: ReftestImageComparison, + test: &ReftestImage, + reference: &ReftestImage, + ) -> bool { + match comparison { + ReftestImageComparison::Equal => { + true + } + ReftestImageComparison::NotEqual { difference_histogram, max_difference, count_different } => { + // Each entry in the sorted self.fuzziness list represents a bucket which + // allows at most num_differences pixels with a difference of at most + // max_difference -- but with the caveat that a difference which is small + // enough to be less than a max_difference of an earlier bucket, must be + // counted against that bucket. + // + // Thus the test will fail if the number of pixels with a difference + // > fuzzy[j-1].max_difference and <= fuzzy[j].max_difference + // exceeds fuzzy[j].num_differences. + // + // (For the first entry, consider fuzzy[j-1] to allow zero pixels of zero + // difference). + // + // For example, say we have this histogram of differences: + // + // | [0] [1] [2] [3] [4] [5] [6] ... [255] + // ------+------------------------------------------ + // Hist. | 0 3 2 1 6 2 0 ... 0 + // + // Ie. image comparison found 3 pixels that differ by 1, 2 that differ by 2, etc. + // (Note that entry 0 is always zero, we don't count matching pixels.) + // + // First we calculate an inclusive prefix sum: + // + // | [0] [1] [2] [3] [4] [5] [6] ... [255] + // ------+------------------------------------------ + // Hist. | 0 3 2 1 6 2 0 ... 0 + // Sum | 0 3 5 6 12 14 14 ... 14 + // + // Let's say the fuzzy statements are: + // Fuzzy( 2, 6 ) -- allow up to 6 pixels that differ by 2 or less + // Fuzzy( 4, 8 ) -- allow up to 8 pixels that differ by 4 or less _but_ + // also by more than 2 (= by 3 or 4). + // + // The first check is Sum[2] <= max 6 which passes: 5 <= 6. + // The second check is Sum[4] - Sum[2] <= max 8 which passes: 12-5 <= 8. + // Finally we check if there are any pixels that exceed the max difference (4) + // by checking Sum[255] - Sum[4] which shows there are 14-12 == 2 so we fail. + + let prefix_sum = difference_histogram.iter() + .scan(0, |sum, i| { *sum += i; Some(*sum) }) + .collect::<Vec<_>>(); + + // check each fuzzy statement for violations. + assert_eq!(0, difference_histogram[0]); + assert_eq!(0, prefix_sum[0]); + + // loop invariant: this is the max_difference of the previous iteration's 'fuzzy' + let mut previous_max_diff = 0; + + // loop invariant: this is the number of pixels to ignore as they have been counted + // against previous iterations' fuzzy statements. + let mut previous_sum_fail = 0; // == prefix_sum[previous_max_diff] + + let mut is_failing = false; + let mut fail_text = String::new(); + + for fuzzy in &self.fuzziness { + let fuzzy_max_difference = cmp::min(255, fuzzy.max_difference); + let num_differences = prefix_sum[fuzzy_max_difference] - previous_sum_fail; + if num_differences > fuzzy.num_differences { + fail_text.push_str( + &format!("{} differences > {} and <= {} (allowed {}); ", + num_differences, + previous_max_diff, fuzzy_max_difference, + fuzzy.num_differences)); + is_failing = true; + } + previous_max_diff = fuzzy_max_difference; + previous_sum_fail = prefix_sum[previous_max_diff]; + } + // do we have any pixels with a difference above the highest allowed + // max difference? if so, we fail the test: + let num_differences = prefix_sum[255] - previous_sum_fail; + if num_differences > 0 { + fail_text.push_str( + &format!("{} num_differences > {} and <= {} (allowed {}); ", + num_differences, + previous_max_diff, 255, + 0)); + is_failing = true; + } + + if is_failing { + println!( + "REFTEST TEST-UNEXPECTED-FAIL | {} | \ + image comparison, max difference: {}, number of differing pixels: {} | {}", + self, + max_difference, + count_different, + fail_text, + ); + println!("REFTEST IMAGE 1 (TEST): {}", test.clone().create_data_uri()); + println!( + "REFTEST IMAGE 2 (REFERENCE): {}", + reference.clone().create_data_uri() + ); + println!("REFTEST TEST-END | {}", self); + + false + } else { + true + } + } + } + } + + /// Report details of the negative case + fn report_unexpected_equality(&self) { + println!("REFTEST TEST-UNEXPECTED-FAIL | {} | image comparison", self); + println!("REFTEST TEST-END | {}", self); + } +} + +impl Display for Reftest { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + let paths: Vec<String> = self.test.iter().map(|t| t.display().to_string()).collect(); + write!( + f, + "{} {} {}", + paths.join(", "), + self.op, + self.reference.display() + ) + } +} + +#[derive(Clone)] +pub struct ReftestImage { + pub data: Vec<u8>, + pub size: DeviceIntSize, +} + +#[derive(Debug, Clone)] +pub enum ReftestImageComparison { + Equal, + NotEqual { + /// entry[j] = number of pixels with a difference of exactly j + difference_histogram: Vec<usize>, + max_difference: usize, + count_different: usize, + }, +} + +impl ReftestImage { + pub fn compare(&self, other: &ReftestImage) -> ReftestImageComparison { + assert_eq!(self.size, other.size); + assert_eq!(self.data.len(), other.data.len()); + assert_eq!(self.data.len() % 4, 0); + + let mut histogram = [0usize; 256]; + let mut count = 0; + let mut max = 0; + + for (a, b) in self.data.chunks(4).zip(other.data.chunks(4)) { + if a != b { + let pixel_max = a.iter() + .zip(b.iter()) + .map(|(x, y)| (*x as isize - *y as isize).abs() as usize) + .max() + .unwrap(); + + count += 1; + assert!(pixel_max < 256, "pixel values are not 8 bit, update the histogram binning code"); + // deliberately avoid counting pixels that match -- + // histogram[0] stays at zero. + // this helps our prefix sum later during analysis to + // only count actual differences. + histogram[pixel_max as usize] += 1; + max = cmp::max(max, pixel_max); + } + } + + if count != 0 { + ReftestImageComparison::NotEqual { + difference_histogram: histogram.to_vec(), + max_difference: max, + count_different: count, + } + } else { + ReftestImageComparison::Equal + } + } + + pub fn create_data_uri(mut self) -> String { + let width = self.size.width; + let height = self.size.height; + + // flip image vertically (texture is upside down) + let orig_pixels = self.data.clone(); + let stride = width as usize * 4; + for y in 0 .. height as usize { + let dst_start = y * stride; + let src_start = (height as usize - y - 1) * stride; + let src_slice = &orig_pixels[src_start .. src_start + stride]; + (&mut self.data[dst_start .. dst_start + stride]) + .clone_from_slice(&src_slice[.. stride]); + } + + let mut png: Vec<u8> = vec![]; + { + let encoder = PNGEncoder::new(&mut png); + encoder + .encode(&self.data[..], width as u32, height as u32, ColorType::Rgba8) + .expect("Unable to encode PNG!"); + } + let png_base64 = base64::encode(&png); + format!("data:image/png;base64,{}", png_base64) + } +} + +struct ReftestManifest { + reftests: Vec<Reftest>, +} +impl ReftestManifest { + fn new(manifest: &Path, environment: &ReftestEnvironment, options: &ReftestOptions) -> ReftestManifest { + let dir = manifest.parent().unwrap(); + let f = + File::open(manifest).unwrap_or_else(|_| panic!("couldn't open manifest: {}", manifest.display())); + let file = BufReader::new(&f); + + let mut reftests = Vec::new(); + + for line in file.lines() { + let l = line.unwrap(); + + // strip the comments + let s = &l[0 .. l.find('#').unwrap_or(l.len())]; + let s = s.trim(); + if s.is_empty() { + continue; + } + + let tokens: Vec<&str> = s.split_whitespace().collect(); + + let mut fuzziness = Vec::new(); + let mut op = None; + let mut font_render_mode = None; + let mut extra_checks = vec![]; + let mut allow_mipmaps = false; + let mut force_subpixel_aa_where_possible = None; + let mut max_surface_override = None; + + let mut parse_command = |token: &str| -> bool { + match token { + function if function.starts_with("force_subpixel_aa_where_possible(") => { + let (_, args, _) = parse_function(function); + force_subpixel_aa_where_possible = Some(args[0].parse().unwrap()); + } + function if function.starts_with("fuzzy-range(") || + function.starts_with("fuzzy-range-if(") => { + let (_, mut args, _) = parse_function(function); + if function.starts_with("fuzzy-range-if(") { + if !environment.parse_condition(args.remove(0)).expect("unknown condition") { + return true; + } + fuzziness.clear(); + } + let num_range = args.len() / 2; + for range in 0..num_range { + let mut max = args[range * 2 ]; + let mut num = args[range * 2 + 1]; + if max.starts_with("<=") { // trim_start_matches would allow <=<=123 + max = &max[2..]; + } + if num.starts_with('*') { + num = &num[1..]; + } + let max_difference = max.parse().unwrap(); + let num_differences = num.parse().unwrap(); + fuzziness.push(RefTestFuzzy { max_difference, num_differences }); + } + } + function if function.starts_with("fuzzy(") || + function.starts_with("fuzzy-if(") => { + let (_, mut args, _) = parse_function(function); + if function.starts_with("fuzzy-if(") { + if !environment.parse_condition(args.remove(0)).expect("unknown condition") { + return true; + } + fuzziness.clear(); + } + let max_difference = args[0].parse().unwrap(); + let num_differences = args[1].parse().unwrap(); + assert!(fuzziness.is_empty()); // if this fires, consider fuzzy-range instead + fuzziness.push(RefTestFuzzy { max_difference, num_differences }); + } + function if function.starts_with("draw_calls(") => { + let (_, args, _) = parse_function(function); + extra_checks.push(ExtraCheck::DrawCalls(args[0].parse().unwrap())); + } + function if function.starts_with("alpha_targets(") => { + let (_, args, _) = parse_function(function); + extra_checks.push(ExtraCheck::AlphaTargets(args[0].parse().unwrap())); + } + function if function.starts_with("color_targets(") => { + let (_, args, _) = parse_function(function); + extra_checks.push(ExtraCheck::ColorTargets(args[0].parse().unwrap())); + } + function if function.starts_with("max_surface_size(") => { + let (_, args, _) = parse_function(function); + max_surface_override = Some(args[0].parse().unwrap()); + } + options if options.starts_with("options(") => { + let (_, args, _) = parse_function(options); + if args.iter().any(|arg| arg == &OPTION_DISABLE_SUBPX) { + font_render_mode = Some(FontRenderMode::Alpha); + } + if args.iter().any(|arg| arg == &OPTION_DISABLE_AA) { + font_render_mode = Some(FontRenderMode::Mono); + } + if args.iter().any(|arg| arg == &OPTION_ALLOW_MIPMAPS) { + allow_mipmaps = true; + } + } + _ => return false, + } + true + }; + + let mut paths = vec![]; + for (i, token) in tokens.iter().enumerate() { + match *token { + "include" => { + assert!(i == 0, "include must be by itself"); + let include = dir.join(tokens[1]); + + reftests.append( + &mut ReftestManifest::new(include.as_path(), environment, options).reftests, + ); + + break; + } + "==" => { + op = Some(ReftestOp::Equal); + } + "!=" => { + op = Some(ReftestOp::NotEqual); + } + "**" => { + op = Some(ReftestOp::Accurate); + } + "!*" => { + op = Some(ReftestOp::Inaccurate); + } + cond if cond.starts_with("if(") => { + let (_, args, _) = parse_function(cond); + if environment.parse_condition(args[0]).expect("unknown condition") { + for command in &args[1..] { + parse_command(command); + } + } + } + command if parse_command(command) => {} + _ => { + match environment.parse_condition(*token) { + Some(true) => {} + Some(false) => break, + _ => paths.push(dir.join(*token)), + } + } + } + } + + // Don't try to add tests for include lines. + if op.is_none() { + assert!(paths.is_empty(), "paths = {:?}", paths); + continue; + } + let op = op.unwrap(); + + // The reference is the last path provided. If multiple paths are + // passed for the test, they render sequentially before being + // compared to the reference, which is useful for testing + // invalidation. + let reference = paths.pop().unwrap(); + let test = paths; + + if environment.platform == "android" { + // Add some fuzz on mobile as we do for non-wrench reftests. + // First remove the ranges with difference <= 2, otherwise they might cause the + // test to fail before the new range is picked up. + fuzziness.retain(|fuzzy| fuzzy.max_difference > 2); + fuzziness.push(RefTestFuzzy { max_difference: 2, num_differences: std::usize::MAX }); + } + + // to avoid changing the meaning of existing tests, the case of + // only a single (or no) 'fuzzy' keyword means we use the max + // of that fuzzy and options.allow_.. (we don't want that to + // turn into a test that allows fuzzy.allow_ *plus* options.allow_): + match fuzziness.len() { + 0 => fuzziness.push(RefTestFuzzy { + max_difference: options.allow_max_difference, + num_differences: options.allow_num_differences }), + 1 => { + let mut fuzzy = &mut fuzziness[0]; + fuzzy.max_difference = cmp::max(fuzzy.max_difference, options.allow_max_difference); + fuzzy.num_differences = cmp::max(fuzzy.num_differences, options.allow_num_differences); + }, + _ => { + // ignore options, use multiple fuzzy keywords instead. make sure + // the list is sorted to speed up counting violations. + fuzziness.sort_by(|a, b| a.max_difference.cmp(&b.max_difference)); + for pair in fuzziness.windows(2) { + if pair[0].max_difference == pair[1].max_difference { + println!("Warning: repeated fuzzy of max_difference {} ignored.", + pair[1].max_difference); + } + } + } + } + + reftests.push(Reftest { + op, + test, + reference, + font_render_mode, + fuzziness, + extra_checks, + allow_mipmaps, + force_subpixel_aa_where_possible, + max_surface_override, + }); + } + + ReftestManifest { reftests } + } + + fn find(&self, prefix: &Path) -> Vec<&Reftest> { + self.reftests + .iter() + .filter(|x| { + x.test.iter().any(|t| t.starts_with(prefix)) || x.reference.starts_with(prefix) + }) + .collect() + } +} + +struct YamlRenderOutput { + image: ReftestImage, + results: RenderResults, +} + +struct ReftestEnvironment { + pub platform: &'static str, + pub version: Option<semver::Version>, + pub mode: &'static str, +} + +impl ReftestEnvironment { + fn new(wrench: &Wrench, window: &WindowWrapper) -> Self { + Self { + platform: Self::platform(wrench, window), + version: Self::version(wrench, window), + mode: Self::mode(), + } + } + + fn has(&self, condition: &str) -> bool { + if self.platform == condition || self.mode == condition { + return true; + } + if let (Some(v), Ok(r)) = (&self.version, &semver::VersionReq::parse(condition)) { + if r.matches(v) { + return true; + } + } + let envkey = format!("WRENCH_REFTEST_CONDITION_{}", condition.to_uppercase()); + env::var(envkey).is_ok() + } + + fn platform(_wrench: &Wrench, window: &WindowWrapper) -> &'static str { + if window.is_software() { + "swgl" + } else if cfg!(target_os = "windows") { + "win" + } else if cfg!(target_os = "linux") { + "linux" + } else if cfg!(target_os = "macos") { + "mac" + } else if cfg!(target_os = "android") { + "android" + } else { + "other" + } + } + + fn version(_wrench: &Wrench, window: &WindowWrapper) -> Option<semver::Version> { + if window.is_software() { + None + } else if cfg!(target_os = "macos") { + use std::str; + let version_bytes = Command::new("defaults") + .arg("read") + .arg("loginwindow") + .arg("SystemVersionStampAsString") + .output() + .expect("Failed to get macOS version") + .stdout; + let mut version_string = str::from_utf8(&version_bytes) + .expect("Failed to read macOS version") + .trim() + .to_string(); + // On some machines this produces just the major.minor and on + // some machines this gives major.minor.patch. But semver requires + // the patch so we fake one if it's not there. + if version_string.chars().filter(|c| *c == '.').count() == 1 { + version_string.push_str(".0"); + } + Some(semver::Version::parse(&version_string) + .unwrap_or_else(|_| panic!("Failed to parse macOS version {}", version_string))) + } else { + None + } + } + + fn mode() -> &'static str { + if cfg!(debug_assertions) { + "debug" + } else { + "release" + } + } + + fn parse_condition(&self, token: &str) -> Option<bool> { + match token { + platform if platform.starts_with("skip_on(") => { + // e.g. skip_on(android,debug) will skip only when + // running on a debug android build. + let (_, args, _) = parse_function(platform); + Some(!args.iter().all(|arg| self.has(arg))) + } + platform if platform.starts_with("env(") => { + // non-negated version of skip_on for nested conditions + let (_, args, _) = parse_function(platform); + Some(args.iter().all(|arg| self.has(arg))) + } + platform if platform.starts_with("platform(") => { + let (_, args, _) = parse_function(platform); + // Skip due to platform not matching + Some(args.iter().any(|arg| arg == &self.platform)) + } + op if op.starts_with("not(") => { + let (_, args, _) = parse_function(op); + Some(!self.parse_condition(args[0]).expect("unknown condition")) + } + op if op.starts_with("or(") => { + let (_, args, _) = parse_function(op); + Some(args.iter().any(|arg| self.parse_condition(arg).expect("unknown condition"))) + } + op if op.starts_with("and(") => { + let (_, args, _) = parse_function(op); + Some(args.iter().all(|arg| self.parse_condition(arg).expect("unknown condition"))) + } + _ => None, + } + } +} + +pub struct ReftestHarness<'a> { + wrench: &'a mut Wrench, + window: &'a mut WindowWrapper, + rx: &'a Receiver<NotifierEvent>, + environment: ReftestEnvironment, +} +impl<'a> ReftestHarness<'a> { + pub fn new(wrench: &'a mut Wrench, window: &'a mut WindowWrapper, rx: &'a Receiver<NotifierEvent>) -> Self { + let environment = ReftestEnvironment::new(wrench, window); + ReftestHarness { wrench, window, rx, environment } + } + + pub fn run(mut self, base_manifest: &Path, reftests: Option<&Path>, options: &ReftestOptions) -> usize { + let manifest = ReftestManifest::new(base_manifest, &self.environment, options); + let reftests = manifest.find(reftests.unwrap_or(&PathBuf::new())); + + let mut total_passing = 0; + let mut failing = Vec::new(); + + for t in reftests { + if self.run_reftest(t) { + total_passing += 1; + } else { + failing.push(t); + } + } + + println!( + "REFTEST INFO | {} passing, {} failing", + total_passing, + failing.len() + ); + + if !failing.is_empty() { + println!("\nReftests with unexpected results:"); + + for reftest in &failing { + println!("\t{}", reftest); + } + } + + failing.len() + } + + fn run_reftest(&mut self, t: &Reftest) -> bool { + let test_name = t.to_string(); + println!("REFTEST {}", test_name); + profile_scope!("wrench reftest", text: &test_name); + + self.wrench + .api + .send_debug_cmd( + DebugCommand::ClearCaches(ClearCache::all()) + ); + + let quality_settings = QualitySettings { + force_subpixel_aa_where_possible: t.force_subpixel_aa_where_possible.unwrap_or_default(), + }; + + self.wrench.set_quality_settings(quality_settings); + + if let Some(max_surface_override) = t.max_surface_override { + self.wrench + .api + .send_debug_cmd( + DebugCommand::SetMaximumSurfaceSize(Some(max_surface_override)) + ); + } + + let window_size = self.window.get_inner_size(); + let reference_image = match t.reference.extension().unwrap().to_str().unwrap() { + "yaml" => None, + "png" => Some(self.load_image(t.reference.as_path(), ImageFormat::Png)), + other => panic!("Unknown reftest extension: {}", other), + }; + let test_size = reference_image.as_ref().map_or(window_size, |img| img.size); + + // The reference can be smaller than the window size, in which case + // we only compare the intersection. + // + // Note also that, when we have multiple test scenes in sequence, we + // want to test the picture caching machinery. But since picture caching + // only takes effect after the result has been the same several frames in + // a row, we need to render the scene multiple times. + let mut images = vec![]; + let mut results = vec![]; + + match t.op { + ReftestOp::Equal | ReftestOp::NotEqual => { + // For equality tests, render each test image and store result + for filename in t.test.iter() { + let output = self.render_yaml( + filename, + test_size, + t.font_render_mode, + t.allow_mipmaps, + ); + images.push(output.image); + results.push(output.results); + } + } + ReftestOp::Accurate | ReftestOp::Inaccurate => { + // For accuracy tests, render the reference yaml at an arbitrary series + // of tile sizes, and compare to the reference drawn at normal tile size. + let tile_sizes = [ + DeviceIntSize::new(128, 128), + DeviceIntSize::new(256, 256), + DeviceIntSize::new(512, 512), + ]; + + for tile_size in &tile_sizes { + self.wrench + .api + .send_debug_cmd( + DebugCommand::SetPictureTileSize(Some(*tile_size)) + ); + + let output = self.render_yaml( + &t.reference, + test_size, + t.font_render_mode, + t.allow_mipmaps, + ); + images.push(output.image); + results.push(output.results); + } + + self.wrench + .api + .send_debug_cmd( + DebugCommand::SetPictureTileSize(None) + ); + } + } + + let reference = if let Some(image) = reference_image { + let save_all_png = false; // flip to true to update all the tests! + if save_all_png { + let img = images.last().unwrap(); + save_flipped(&t.reference, img.data.clone(), img.size); + } + image + } else { + let output = self.render_yaml( + &t.reference, + test_size, + t.font_render_mode, + t.allow_mipmaps, + ); + output.image + }; + + if let Some(_) = t.max_surface_override { + self.wrench + .api + .send_debug_cmd( + DebugCommand::SetMaximumSurfaceSize(None) + ); + } + + for extra_check in t.extra_checks.iter() { + if !extra_check.run(&results) { + println!( + "REFTEST TEST-UNEXPECTED-FAIL | {} | Failing Check: {:?} | Actual Results: {:?}", + t, + extra_check, + results, + ); + println!("REFTEST TEST-END | {}", t); + return false; + } + } + + match t.op { + ReftestOp::Equal => { + // Ensure that the final image matches the reference + let test = images.pop().unwrap(); + let comparison = test.compare(&reference); + t.check_and_report_equality_failure( + comparison, + &test, + &reference, + ) + } + ReftestOp::NotEqual => { + // Ensure that the final image *doesn't* match the reference + let test = images.pop().unwrap(); + let comparison = test.compare(&reference); + match comparison { + ReftestImageComparison::Equal => { + t.report_unexpected_equality(); + false + } + ReftestImageComparison::NotEqual { .. } => { + true + } + } + } + ReftestOp::Accurate => { + // Ensure that *all* images match the reference + for test in images.drain(..) { + let comparison = test.compare(&reference); + + if !t.check_and_report_equality_failure( + comparison, + &test, + &reference, + ) { + return false; + } + } + + true + } + ReftestOp::Inaccurate => { + // Ensure that at least one of the images doesn't match the reference + let all_same = images.iter().all(|image| { + match image.compare(&reference) { + ReftestImageComparison::Equal => true, + ReftestImageComparison::NotEqual { .. } => false, + } + }); + + if all_same { + t.report_unexpected_equality(); + } + + !all_same + } + } + } + + fn load_image(&mut self, filename: &Path, format: ImageFormat) -> ReftestImage { + let file = BufReader::new(File::open(filename).unwrap()); + let img_raw = load_piston_image(file, format).unwrap(); + let img = img_raw.flipv().to_rgba(); + let size = img.dimensions(); + ReftestImage { + data: img.into_raw(), + size: DeviceIntSize::new(size.0 as i32, size.1 as i32), + } + } + + fn render_yaml( + &mut self, + filename: &Path, + size: DeviceIntSize, + font_render_mode: Option<FontRenderMode>, + allow_mipmaps: bool, + ) -> YamlRenderOutput { + let mut reader = YamlFrameReader::new(filename); + reader.set_font_render_mode(font_render_mode); + reader.allow_mipmaps(allow_mipmaps); + reader.do_frame(self.wrench); + + self.wrench.api.flush_scene_builder(); + + // wait for the frame + self.rx.recv().unwrap(); + let results = self.wrench.render(); + + let window_size = self.window.get_inner_size(); + assert!( + size.width <= window_size.width && + size.height <= window_size.height, + "size={:?} ws={:?}", size, window_size + ); + + // taking the bottom left sub-rectangle + let rect = FramebufferIntRect::from_origin_and_size( + FramebufferIntPoint::new(0, window_size.height - size.height), + FramebufferIntSize::new(size.width, size.height), + ); + let pixels = self.wrench.renderer.read_pixels_rgba8(rect); + self.window.swap_buffers(); + + let write_debug_images = false; + if write_debug_images { + let debug_path = filename.with_extension("yaml.png"); + save_flipped(debug_path, pixels.clone(), size); + } + + reader.deinit(self.wrench); + + YamlRenderOutput { + image: ReftestImage { data: pixels, size }, + results, + } + } +} diff --git a/gfx/wr/wrench/src/test_invalidation.rs b/gfx/wr/wrench/src/test_invalidation.rs new file mode 100644 index 0000000000..4befcb9980 --- /dev/null +++ b/gfx/wr/wrench/src/test_invalidation.rs @@ -0,0 +1,129 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::NotifierEvent; +use crate::WindowWrapper; +use std::path::PathBuf; +use std::sync::mpsc::Receiver; +use crate::wrench::{Wrench, WrenchThing}; +use crate::yaml_frame_reader::YamlFrameReader; +use webrender::{PictureCacheDebugInfo, TileDebugInfo}; +use webrender::api::units::*; + +pub struct TestHarness<'a> { + wrench: &'a mut Wrench, + window: &'a mut WindowWrapper, + rx: Receiver<NotifierEvent>, +} + +struct RenderResult { + pc_debug: PictureCacheDebugInfo, + composite_needed: bool, +} + +// Convenience method to build a picture rect +fn pr(x: f32, y: f32, w: f32, h: f32) -> PictureRect { + PictureRect::from_origin_and_size( + PicturePoint::new(x, y), + PictureSize::new(w, h), + ) +} + +impl<'a> TestHarness<'a> { + pub fn new( + wrench: &'a mut Wrench, + window: &'a mut WindowWrapper, + rx: Receiver<NotifierEvent> + ) -> Self { + TestHarness { + wrench, + window, + rx, + } + } + + /// Main entry point for invalidation tests + pub fn run( + mut self, + ) { + // List all invalidation tests here + self.test_basic(); + self.test_composite_nop(); + } + + /// Simple validation / proof of concept of invalidation testing + fn test_basic( + &mut self, + ) { + // Render basic.yaml, ensure that the valid/dirty rects are as expected + let results = self.render_yaml("basic"); + let tile_info = results.pc_debug.slice(0).tile(0, 0).as_dirty(); + assert_eq!( + tile_info.local_valid_rect, + pr(100.0, 100.0, 500.0, 100.0), + ); + assert_eq!( + tile_info.local_dirty_rect, + pr(100.0, 100.0, 500.0, 100.0), + ); + + // Render it again and ensure the tile was considered valid (no rasterization was done) + let results = self.render_yaml("basic"); + assert_eq!(*results.pc_debug.slice(0).tile(0, 0), TileDebugInfo::Valid); + } + + /// Ensure WR detects composites are needed for position changes within a single tile. + fn test_composite_nop( + &mut self, + ) { + // Render composite_nop_1.yaml, ensure that the valid/dirty rects are as expected + let results = self.render_yaml("composite_nop_1"); + let tile_info = results.pc_debug.slice(0).tile(0, 0).as_dirty(); + assert_eq!( + tile_info.local_valid_rect, + pr(100.0, 100.0, 100.0, 100.0), + ); + assert_eq!( + tile_info.local_dirty_rect, + pr(100.0, 100.0, 100.0, 100.0), + ); + + // Render composite_nop_2.yaml, ensure that the valid/dirty rects are as expected + let results = self.render_yaml("composite_nop_2"); + let tile_info = results.pc_debug.slice(0).tile(0, 0).as_dirty(); + assert_eq!( + tile_info.local_valid_rect, + pr(100.0, 120.0, 100.0, 100.0), + ); + assert_eq!( + tile_info.local_dirty_rect, + pr(100.0, 120.0, 100.0, 100.0), + ); + + // Main part of this test - ensure WR detects a composite is required in this case + assert!(results.composite_needed); + } + + /// Render a YAML file, and return the picture cache debug info + fn render_yaml( + &mut self, + filename: &str, + ) -> RenderResult { + let path = format!("invalidation/{}.yaml", filename); + let mut reader = YamlFrameReader::new(&PathBuf::from(path)); + + reader.do_frame(self.wrench); + let composite_needed = match self.rx.recv().unwrap() { + NotifierEvent::WakeUp { composite_needed } => composite_needed, + NotifierEvent::ShutDown => unreachable!(), + }; + let results = self.wrench.render(); + self.window.swap_buffers(); + + RenderResult { + pc_debug: results.picture_cache_debug, + composite_needed, + } + } +} diff --git a/gfx/wr/wrench/src/test_shaders.rs b/gfx/wr/wrench/src/test_shaders.rs new file mode 100644 index 0000000000..9e6492538a --- /dev/null +++ b/gfx/wr/wrench/src/test_shaders.rs @@ -0,0 +1,161 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use webrender::ShaderKind; +use webrender_build::shader::{ShaderFeatureFlags, ShaderVersion, build_shader_strings}; +use webrender_build::shader::get_shader_features; +use glsl_lang::ast::{InterpolationQualifierData, NodeContent, SingleDeclaration}; +use glsl_lang::ast::{StorageQualifierData, TranslationUnit, TypeSpecifierNonArrayData}; +use glsl_lang::ast::TypeQualifierSpecData; +use glsl_lang::parse::DefaultParse as _; +use glsl_lang::visitor::{Host, Visit, Visitor}; + +/// Tests that a shader contains no flat scalar varyings. +/// These must be avoided on Adreno 3xx devices due to bug 1630356. +fn test_no_flat_scalar_varyings( + name: &str, + shader: &mut TranslationUnit, + _shader_kind: ShaderKind, +) { + struct FlatScalarVaryingsVisitor { + shader_name: String, + } + + impl Visitor for FlatScalarVaryingsVisitor { + fn visit_single_declaration(&mut self, declaration: &SingleDeclaration) -> Visit { + let is_scalar = matches!( + declaration.ty.ty.ty.content, + TypeSpecifierNonArrayData::Bool + | TypeSpecifierNonArrayData::Int + | TypeSpecifierNonArrayData::UInt + | TypeSpecifierNonArrayData::Float + | TypeSpecifierNonArrayData::Double + ); + + let qualifiers = declaration + .ty + .qualifier + .as_ref() + .map(|q| q.qualifiers.as_slice()) + .unwrap_or(&[]); + + let is_flat = qualifiers.contains( + &TypeQualifierSpecData::Interpolation(InterpolationQualifierData::Flat.into_node()) + .into_node(), + ); + + assert!( + !(is_scalar && is_flat), + "{}: {} is a flat scalar varying", + self.shader_name, + &declaration.name.as_ref().unwrap() + ); + + Visit::Parent + } + } + + let mut visitor = FlatScalarVaryingsVisitor { + shader_name: name.to_string(), + }; + shader.visit(&mut visitor); +} + +/// Tests that a shader's varyings have an explicit precision specifier. +/// Mali vendor tooling shows us that we are often varying-iterpolation bound, so using mediump +/// where possible helps alleviate this. By enforcing that varyings are given explicit precisions, +/// we ensure that highp is only used when necessary rather than just by default. +fn test_varying_explicit_precision( + name: &str, + shader: &mut TranslationUnit, + shader_kind: ShaderKind, +) { + struct VaryingExplicitPrecisionVisitor { + shader_name: String, + shader_kind: ShaderKind, + } + + impl Visitor for VaryingExplicitPrecisionVisitor { + fn visit_single_declaration(&mut self, declaration: &SingleDeclaration) -> Visit { + let qualifiers = declaration + .ty + .qualifier + .as_ref() + .map(|q| q.qualifiers.as_slice()) + .unwrap_or(&[]); + + let is_varying = qualifiers.iter().any(|qualifier| { + match &qualifier.content { + TypeQualifierSpecData::Storage(storage) => match self.shader_kind { + ShaderKind::Vertex => storage.content == StorageQualifierData::Out, + ShaderKind::Fragment => storage.content == StorageQualifierData::In, + } + _ => false, + } + }); + + let has_explicit_precision = qualifiers + .iter() + .any(|qualifier| matches!(qualifier.content, TypeQualifierSpecData::Precision(_))); + + assert!( + !is_varying || has_explicit_precision, + "{}: {} is a varying without an explicit precision declared", + self.shader_name, + &declaration.name.as_ref().unwrap() + ); + + Visit::Parent + } + } + + let mut visitor = VaryingExplicitPrecisionVisitor { + shader_name: name.to_string(), + shader_kind, + }; + shader.visit(&mut visitor); +} + +pub fn test_shaders() { + let mut flags = ShaderFeatureFlags::all(); + if cfg!(any(target_os = "windows", target_os = "android")) { + flags.remove(ShaderFeatureFlags::GL); + } else { + flags.remove(ShaderFeatureFlags::GLES); + } + // glsl-lang crate fails to parse advanced blend shaders + flags.remove(ShaderFeatureFlags::ADVANCED_BLEND_EQUATION); + + for (shader, configs) in get_shader_features(flags) { + for config in configs { + let name = if config.is_empty() { + shader.to_string() + } else { + format!("{}_{}", shader, config.replace(",", "_")) + }; + let vert_name = format!("{}.vert", name); + let frag_name = format!("{}.frag", name); + + + let features = config + .split(",") + .filter(|f| !f.is_empty()) + .collect::<Vec<_>>(); + + let (vert_src, frag_src) = + build_shader_strings(ShaderVersion::Gles, &features, shader, &|f| { + webrender::get_unoptimized_shader_source(f, None) + }); + + let mut vert = TranslationUnit::parse(&vert_src).unwrap(); + let mut frag = TranslationUnit::parse(&frag_src).unwrap(); + + + test_no_flat_scalar_varyings(&vert_name, &mut vert, ShaderKind::Vertex); + test_no_flat_scalar_varyings(&frag_name, &mut frag, ShaderKind::Fragment); + test_varying_explicit_precision(&vert_name, &mut vert, ShaderKind::Vertex); + test_varying_explicit_precision(&frag_name, &mut frag, ShaderKind::Fragment); + } + } +} diff --git a/gfx/wr/wrench/src/wrench.rs b/gfx/wr/wrench/src/wrench.rs new file mode 100644 index 0000000000..4116ff1726 --- /dev/null +++ b/gfx/wr/wrench/src/wrench.rs @@ -0,0 +1,641 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +use crate::blob; +use crossbeam::sync::chase_lev; +#[cfg(windows)] +use dwrote; +#[cfg(all(unix, not(target_os = "android")))] +use font_loader::system_fonts; +use winit::event_loop::EventLoopProxy; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; +use std::sync::mpsc::Receiver; +use webrender::api::*; +use webrender::render_api::*; +use webrender::api::units::*; +use webrender::{DebugFlags, RenderResults, ShaderPrecacheFlags}; +use crate::{WindowWrapper, NotifierEvent}; + +// TODO(gw): This descriptor matches what we currently support for fonts +// but is quite a mess. We should at least document and +// use better types for things like the style and stretch. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum FontDescriptor { + Path { path: PathBuf, font_index: u32 }, + Family { name: String }, + Properties { + family: String, + weight: u32, + style: u32, + stretch: u32, + }, +} + +struct NotifierData { + events_loop_proxy: Option<EventLoopProxy<()>>, + frames_notified: u32, + timing_receiver: chase_lev::Stealer<time::SteadyTime>, + verbose: bool, +} + +impl NotifierData { + fn new( + events_loop_proxy: Option<EventLoopProxy<()>>, + timing_receiver: chase_lev::Stealer<time::SteadyTime>, + verbose: bool, + ) -> Self { + NotifierData { + events_loop_proxy, + frames_notified: 0, + timing_receiver, + verbose, + } + } +} + +struct Notifier(Arc<Mutex<NotifierData>>); + +impl Notifier { + fn update(&self, check_document: bool) { + let mut data = self.0.lock(); + let data = data.as_mut().unwrap(); + if check_document { + match data.timing_receiver.steal() { + chase_lev::Steal::Data(last_timing) => { + data.frames_notified += 1; + if data.verbose && data.frames_notified == 600 { + let elapsed = time::SteadyTime::now() - last_timing; + println!( + "frame latency (consider queue depth here): {:3.6} ms", + elapsed.num_microseconds().unwrap() as f64 / 1000. + ); + data.frames_notified = 0; + } + } + _ => { + println!("Notified of frame, but no frame was ready?"); + } + } + } + + if let Some(ref _elp) = data.events_loop_proxy { + #[cfg(not(target_os = "android"))] + let _ = _elp.send_event(()); + } + } +} + +impl RenderNotifier for Notifier { + fn clone(&self) -> Box<dyn RenderNotifier> { + Box::new(Notifier(self.0.clone())) + } + + fn wake_up(&self, _composite_needed: bool) { + self.update(false); + } + + fn new_frame_ready(&self, _: DocumentId, + scrolled: bool, + _composite_needed: bool, + _: FramePublishId) { + self.update(!scrolled); + } +} + +pub trait WrenchThing { + fn next_frame(&mut self); + fn prev_frame(&mut self); + fn do_frame(&mut self, _: &mut Wrench) -> u32; +} + +impl WrenchThing for CapturedDocument { + fn next_frame(&mut self) {} + fn prev_frame(&mut self) {} + fn do_frame(&mut self, wrench: &mut Wrench) -> u32 { + if let Some(root_pipeline_id) = self.root_pipeline_id.take() { + // skip the first frame - to not overwrite the loaded one + let mut txn = Transaction::new(); + txn.set_root_pipeline(root_pipeline_id); + wrench.api.send_transaction(self.document_id, txn); + } else { + wrench.refresh(); + } + 0 + } +} + +pub struct CapturedSequence { + root: PathBuf, + frame: usize, + frame_set: Vec<(u32, u32)>, +} + +impl CapturedSequence { + pub fn new(root: PathBuf, scene_start: u32, frame_start: u32) -> Self { + // Build set of a scene and frame IDs. + let mut scene = scene_start; + let mut frame = frame_start; + let mut frame_set = Vec::new(); + while Self::scene_root(&root, scene).as_path().is_dir() { + while Self::frame_root(&root, scene, frame).as_path().is_dir() { + frame_set.push((scene, frame)); + frame += 1; + } + scene += 1; + frame = 1; + } + + assert!(!frame_set.is_empty()); + + Self { + root, + frame: 0, + frame_set, + } + } + + fn scene_root(root: &Path, scene: u32) -> PathBuf { + let path = format!("scenes/{:05}", scene); + root.join(path) + } + + fn frame_root(root: &Path, scene: u32, frame: u32) -> PathBuf { + let path = format!("scenes/{:05}/frames/{:05}", scene, frame); + root.join(path) + } +} + +impl WrenchThing for CapturedSequence { + fn next_frame(&mut self) { + if self.frame + 1 < self.frame_set.len() { + self.frame += 1; + } + } + + fn prev_frame(&mut self) { + if self.frame > 0 { + self.frame -= 1; + } + } + + fn do_frame(&mut self, wrench: &mut Wrench) -> u32 { + let mut documents = wrench.api.load_capture(self.root.clone(), Some(self.frame_set[self.frame])); + println!("loaded {:?} from {:?}", + documents.iter().map(|cd| cd.document_id).collect::<Vec<_>>(), + self.frame_set[self.frame]); + let captured = documents.swap_remove(0); + wrench.document_id = captured.document_id; + self.frame as u32 + } +} + +pub struct Wrench { + window_size: DeviceIntSize, + + pub renderer: webrender::Renderer, + pub api: RenderApi, + pub document_id: DocumentId, + pub root_pipeline_id: PipelineId, + + window_title_to_set: Option<String>, + + graphics_api: webrender::GraphicsApiInfo, + + pub rebuild_display_lists: bool, + pub verbose: bool, + + pub frame_start_sender: chase_lev::Worker<time::SteadyTime>, + + pub callbacks: Arc<Mutex<blob::BlobCallbacks>>, +} + +impl Wrench { + #[allow(clippy::too_many_arguments)] + pub fn new( + window: &mut WindowWrapper, + proxy: Option<EventLoopProxy<()>>, + shader_override_path: Option<PathBuf>, + use_optimized_shaders: bool, + size: DeviceIntSize, + do_rebuild: bool, + no_subpixel_aa: bool, + verbose: bool, + no_scissor: bool, + no_batch: bool, + precache_shaders: bool, + dump_shader_source: Option<String>, + notifier: Option<Box<dyn RenderNotifier>>, + ) -> Self { + println!("Shader override path: {:?}", shader_override_path); + + let mut debug_flags = DebugFlags::ECHO_DRIVER_MESSAGES; + debug_flags.set(DebugFlags::DISABLE_BATCHING, no_batch); + let callbacks = Arc::new(Mutex::new(blob::BlobCallbacks::new())); + + let precache_flags = if precache_shaders { + ShaderPrecacheFlags::FULL_COMPILE + } else { + ShaderPrecacheFlags::empty() + }; + + let opts = webrender::WebRenderOptions { + resource_override_path: shader_override_path, + use_optimized_shaders, + enable_subpixel_aa: !no_subpixel_aa, + debug_flags, + enable_clear_scissor: no_scissor.then_some(false), + max_recorded_profiles: 16, + precache_flags, + blob_image_handler: Some(Box::new(blob::CheckerboardRenderer::new(callbacks.clone()))), + testing: true, + max_internal_texture_size: Some(8196), // Needed for rawtest::test_resize_image. + allow_advanced_blend_equation: window.is_software(), + dump_shader_source, + // SWGL doesn't support the GL_ALWAYS depth comparison function used by + // `clear_caches_with_quads`, but scissored clears work well. + clear_caches_with_quads: !window.is_software(), + ..Default::default() + }; + + // put an Awakened event into the queue to kick off the first frame + if let Some(ref _elp) = proxy { + #[cfg(not(target_os = "android"))] + let _ = _elp.send_event(()); + } + + let (timing_sender, timing_receiver) = chase_lev::deque(); + let notifier = notifier.unwrap_or_else(|| { + let data = Arc::new(Mutex::new(NotifierData::new(proxy, timing_receiver, verbose))); + Box::new(Notifier(data)) + }); + + let (renderer, sender) = webrender::create_webrender_instance( + window.clone_gl(), + notifier, + opts, + None, + ).unwrap(); + + let api = sender.create_api(); + let document_id = api.add_document(size); + + let graphics_api = renderer.get_graphics_api_info(); + + let mut wrench = Wrench { + window_size: size, + + renderer, + api, + document_id, + window_title_to_set: None, + + rebuild_display_lists: do_rebuild, + verbose, + + root_pipeline_id: PipelineId(0, 0), + + graphics_api, + frame_start_sender: timing_sender, + + callbacks, + }; + + wrench.set_title("start"); + let mut txn = Transaction::new(); + txn.set_root_pipeline(wrench.root_pipeline_id); + wrench.api.send_transaction(wrench.document_id, txn); + + wrench + } + + pub fn set_quality_settings(&mut self, settings: QualitySettings) { + let mut txn = Transaction::new(); + txn.set_quality_settings(settings); + self.api.send_transaction(self.document_id, txn); + } + + pub fn layout_simple_ascii( + &mut self, + font_key: FontKey, + instance_key: FontInstanceKey, + text: &str, + size: f32, + origin: LayoutPoint, + flags: FontInstanceFlags, + ) -> (Vec<u32>, Vec<LayoutPoint>, LayoutRect) { + // Map the string codepoints to glyph indices in this font. + // Just drop any glyph that isn't present in this font. + let indices: Vec<u32> = self.api + .get_glyph_indices(font_key, text) + .iter() + .filter_map(|idx| *idx) + .collect(); + + // Retrieve the metrics for each glyph. + let metrics = self.api.get_glyph_dimensions(instance_key, indices.clone()); + + let mut bounding_rect = LayoutRect::zero(); + let mut positions = Vec::new(); + + let mut cursor = origin; + let direction = if flags.contains(FontInstanceFlags::TRANSPOSE) { + LayoutVector2D::new( + 0.0, + if flags.contains(FontInstanceFlags::FLIP_Y) { -1.0 } else { 1.0 }, + ) + } else { + LayoutVector2D::new( + if flags.contains(FontInstanceFlags::FLIP_X) { -1.0 } else { 1.0 }, + 0.0, + ) + }; + for metric in metrics { + positions.push(cursor); + + if let Some(GlyphDimensions { left, top, width, height, advance }) = metric { + let glyph_rect = LayoutRect::from_origin_and_size( + LayoutPoint::new(cursor.x + left as f32, cursor.y - top as f32), + LayoutSize::new(width as f32, height as f32) + ); + bounding_rect = bounding_rect.union(&glyph_rect); + cursor += direction * advance; + } else { + // Extract the advances from the metrics. The get_glyph_dimensions API + // has a limitation that it can't currently get dimensions for non-renderable + // glyphs (e.g. spaces), so just use a rough estimate in that case. + let space_advance = size / 3.0; + cursor += direction * space_advance; + } + } + + // The platform font implementations don't always handle + // the exact dimensions used when subpixel AA is enabled + // on glyphs. As a workaround, inflate the bounds by + // 2 pixels on either side, to give a slightly less + // tight fitting bounding rect. + let bounding_rect = bounding_rect.inflate(2.0, 2.0); + + (indices, positions, bounding_rect) + } + + pub fn set_title(&mut self, extra: &str) { + self.window_title_to_set = Some(format!( + "Wrench: {} - {} - {}", + extra, + self.graphics_api.renderer, + self.graphics_api.version + )); + } + + pub fn take_title(&mut self) -> Option<String> { + self.window_title_to_set.take() + } + + pub fn should_rebuild_display_lists(&self) -> bool { + self.rebuild_display_lists + } + + pub fn window_size_f32(&self) -> LayoutSize { + LayoutSize::new( + self.window_size.width as f32, + self.window_size.height as f32, + ) + } + + #[cfg(target_os = "windows")] + pub fn font_key_from_native_handle(&mut self, descriptor: &NativeFontHandle) -> FontKey { + let key = self.api.generate_font_key(); + let mut txn = Transaction::new(); + txn.add_native_font(key, descriptor.clone()); + self.api.send_transaction(self.document_id, txn); + key + } + + #[cfg(target_os = "windows")] + pub fn font_key_from_name(&mut self, font_name: &str) -> FontKey { + self.font_key_from_properties( + font_name, + dwrote::FontWeight::Regular.to_u32(), + dwrote::FontStyle::Normal.to_u32(), + dwrote::FontStretch::Normal.to_u32(), + ) + } + + #[cfg(target_os = "windows")] + pub fn font_key_from_properties( + &mut self, + family: &str, + weight: u32, + style: u32, + stretch: u32, + ) -> FontKey { + let weight = dwrote::FontWeight::from_u32(weight); + let style = dwrote::FontStyle::from_u32(style); + let stretch = dwrote::FontStretch::from_u32(stretch); + let desc = dwrote::FontDescriptor { + family_name: family.to_owned(), + weight, + style, + stretch, + }; + let system_fc = dwrote::FontCollection::system(); + if let Some(font) = system_fc.get_font_from_descriptor(&desc) { + let face = font.create_font_face(); + let files = face.get_files(); + if files.len() == 1 { + if let Some(path) = files[0].get_font_file_path() { + return self.font_key_from_native_handle(&NativeFontHandle { + path, + index: face.get_index(), + }); + } + } + } + panic!("failed loading font from properties {:?}", desc) + } + + #[cfg(all(unix, not(target_os = "android")))] + pub fn font_key_from_properties( + &mut self, + family: &str, + _weight: u32, + _style: u32, + _stretch: u32, + ) -> FontKey { + let property = system_fonts::FontPropertyBuilder::new() + .family(family) + .build(); + let (font, index) = system_fonts::get(&property).unwrap(); + self.font_key_from_bytes(font, index as u32) + } + + #[cfg(target_os = "android")] + pub fn font_key_from_properties( + &mut self, + _family: &str, + _weight: u32, + _style: u32, + _stretch: u32, + ) -> FontKey { + unimplemented!() + } + + #[cfg(all(unix, not(target_os = "android")))] + pub fn font_key_from_name(&mut self, font_name: &str) -> FontKey { + let property = system_fonts::FontPropertyBuilder::new() + .family(font_name) + .build(); + let (font, index) = system_fonts::get(&property).unwrap(); + self.font_key_from_bytes(font, index as u32) + } + + #[cfg(target_os = "android")] + pub fn font_key_from_name(&mut self, _font_name: &str) -> FontKey { + unimplemented!() + } + + pub fn font_key_from_bytes(&mut self, bytes: Vec<u8>, index: u32) -> FontKey { + let key = self.api.generate_font_key(); + let mut txn = Transaction::new(); + txn.add_raw_font(key, bytes, index); + self.api.send_transaction(self.document_id, txn); + key + } + + pub fn add_font_instance(&mut self, + font_key: FontKey, + size: f32, + flags: FontInstanceFlags, + render_mode: Option<FontRenderMode>, + bg_color: Option<ColorU>, + synthetic_italics: SyntheticItalics, + ) -> FontInstanceKey { + let key = self.api.generate_font_instance_key(); + let mut txn = Transaction::new(); + let mut options: FontInstanceOptions = Default::default(); + options.flags |= flags; + if let Some(render_mode) = render_mode { + options.render_mode = render_mode; + } + if let Some(bg_color) = bg_color { + options.bg_color = bg_color; + } + options.synthetic_italics = synthetic_italics; + txn.add_font_instance(key, font_key, size, Some(options), None, Vec::new()); + self.api.send_transaction(self.document_id, txn); + key + } + + #[allow(dead_code)] + pub fn delete_font_instance(&mut self, key: FontInstanceKey) { + let mut txn = Transaction::new(); + txn.delete_font_instance(key); + self.api.send_transaction(self.document_id, txn); + } + + pub fn update(&mut self, dim: DeviceIntSize) { + if dim != self.window_size { + self.window_size = dim; + } + } + + pub fn begin_frame(&mut self) { + self.frame_start_sender.push(time::SteadyTime::now()); + } + + pub fn send_lists( + &mut self, + frame_number: u32, + display_lists: Vec<(PipelineId, BuiltDisplayList)>, + scroll_offsets: &HashMap<ExternalScrollId, Vec<SampledScrollOffset>>, + ) { + let mut txn = Transaction::new(); + for display_list in display_lists { + txn.set_display_list( + Epoch(frame_number), + display_list, + ); + } + + for (id, offsets) in scroll_offsets { + txn.set_scroll_offsets(*id, offsets.clone()); + } + + txn.generate_frame(0, RenderReasons::TESTING); + self.api.send_transaction(self.document_id, txn); + } + + pub fn get_frame_profiles( + &mut self, + ) -> (Vec<webrender::CpuProfile>, Vec<webrender::GpuProfile>) { + self.renderer.get_frame_profiles() + } + + pub fn render(&mut self) -> RenderResults { + self.renderer.update(); + let _ = self.renderer.flush_pipeline_info(); + self.renderer + .render(self.window_size, 0) + .expect("errors encountered during render!") + } + + pub fn refresh(&mut self) { + self.begin_frame(); + let mut txn = Transaction::new(); + txn.generate_frame(0, RenderReasons::TESTING); + self.api.send_transaction(self.document_id, txn); + } + + pub fn show_onscreen_help(&mut self) { + let help_lines = [ + "Esc - Quit", + "H - Toggle help", + "R - Toggle recreating display items each frame", + "P - Toggle profiler", + "O - Toggle showing intermediate targets", + "I - Toggle showing texture caches", + "B - Toggle showing alpha primitive rects", + "V - Toggle showing overdraw", + "G - Toggle showing gpu cache updates", + "S - Toggle compact profiler", + "Q - Toggle GPU queries for time and samples", + "M - Trigger memory pressure event", + "T - Save CPU profile to a file", + "C - Save a capture to captures/wrench/", + "X - Do a hit test at the current cursor position", + "Y - Clear all caches", + ]; + + let color_and_offset = [(ColorF::BLACK, 2.0), (ColorF::WHITE, 0.0)]; + self.renderer.device.begin_frame(); // next line might compile shaders: + let dr = self.renderer.debug_renderer().unwrap(); + + for co in &color_and_offset { + let x = 15.0 + co.1; + let mut y = 15.0 + co.1 + dr.line_height(); + for line in &help_lines { + dr.add_text(x, y, line, co.0.into(), None); + y += dr.line_height(); + } + } + self.renderer.device.end_frame(); + } + + pub fn shut_down(self, rx: Receiver<NotifierEvent>) { + self.api.shut_down(true); + + loop { + match rx.recv() { + Ok(NotifierEvent::ShutDown) => { break; } + Ok(_) => {} + Err(e) => { panic!("Did not shut down properly: {:?}.", e); } + } + } + + self.renderer.deinit(); + } +} diff --git a/gfx/wr/wrench/src/yaml_frame_reader.rs b/gfx/wr/wrench/src/yaml_frame_reader.rs new file mode 100644 index 0000000000..9bd8b56943 --- /dev/null +++ b/gfx/wr/wrench/src/yaml_frame_reader.rs @@ -0,0 +1,2125 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use euclid::SideOffsets2D; +use gleam::gl; +use image::GenericImageView; +use crate::parse_function::parse_function; +use crate::premultiply::premultiply; +use std::collections::HashMap; +use std::convert::TryInto; +use std::fs::File; +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::usize; +use webrender::api::*; +use webrender::render_api::*; +use webrender::api::units::*; +use webrender::api::FillRule; +use crate::wrench::{FontDescriptor, Wrench, WrenchThing}; +use crate::yaml_helper::{StringEnum, YamlHelper, make_perspective}; +use yaml_rust::{Yaml, YamlLoader}; +use crate::PLATFORM_DEFAULT_FACE_NAME; + +macro_rules! try_intersect { + ($first: expr, $second: expr) => { + if let Some(rect) = ($first).intersection($second) { + rect + } else { + warn!("skipping item with non-intersecting bounds and clip_rect"); + return; + } + } +} + +fn rsrc_path(item: &Yaml, aux_dir: &Path) -> PathBuf { + let filename = item.as_str().unwrap(); + let mut file = aux_dir.to_path_buf(); + file.push(filename); + file +} + +impl FontDescriptor { + fn from_yaml(item: &Yaml, aux_dir: &Path) -> FontDescriptor { + if !item["family"].is_badvalue() { + FontDescriptor::Properties { + family: item["family"].as_str().unwrap().to_owned(), + weight: item["weight"].as_i64().unwrap_or(400) as u32, + style: item["style"].as_i64().unwrap_or(0) as u32, + stretch: item["stretch"].as_i64().unwrap_or(5) as u32, + } + } else if !item["font"].is_badvalue() { + let path = rsrc_path(&item["font"], aux_dir); + FontDescriptor::Path { + path, + font_index: item["font-index"].as_i64().unwrap_or(0) as u32, + } + } else { + FontDescriptor::Family { + name: PLATFORM_DEFAULT_FACE_NAME.to_string(), + } + } + } +} + +struct LocalExternalImageHandler { + texture_ids: Vec<(gl::GLuint, ImageDescriptor)>, +} + +impl LocalExternalImageHandler { + pub fn new() -> LocalExternalImageHandler { + LocalExternalImageHandler { + texture_ids: Vec::new(), + } + } + + fn init_gl_texture( + id: gl::GLuint, + gl_target: gl::GLuint, + format_desc: webrender::FormatDesc, + width: gl::GLint, + height: gl::GLint, + bytes: &[u8], + gl: &dyn gl::Gl, + ) { + gl.bind_texture(gl_target, id); + gl.tex_parameter_i(gl_target, gl::TEXTURE_MAG_FILTER, gl::LINEAR as gl::GLint); + gl.tex_parameter_i(gl_target, gl::TEXTURE_MIN_FILTER, gl::LINEAR as gl::GLint); + gl.tex_parameter_i(gl_target, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as gl::GLint); + gl.tex_parameter_i(gl_target, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as gl::GLint); + gl.tex_image_2d( + gl_target, + 0, + format_desc.internal as gl::GLint, + width, + height, + 0, + format_desc.external, + format_desc.pixel_type, + Some(bytes), + ); + gl.bind_texture(gl_target, 0); + } + + pub fn add_image(&mut self, + device: &webrender::Device, + desc: ImageDescriptor, + target: ImageBufferKind, + image_data: ImageData, + ) -> ImageData { + let (image_id, channel_idx) = match image_data { + ImageData::Raw(ref data) => { + let gl = device.gl(); + let texture_ids = gl.gen_textures(1); + let format_desc = if desc.format == ImageFormat::BGRA8 { + // Force BGRA8 data to RGBA8 layout to avoid potential + // need for usage of texture-swizzle. + webrender::FormatDesc { + external: gl::BGRA, + .. device.gl_describe_format(ImageFormat::RGBA8) + } + } else { + device.gl_describe_format(desc.format) + }; + + LocalExternalImageHandler::init_gl_texture( + texture_ids[0], + webrender::get_gl_target(target), + format_desc, + desc.size.width as gl::GLint, + desc.size.height as gl::GLint, + data, + gl, + ); + self.texture_ids.push((texture_ids[0], desc)); + (ExternalImageId((self.texture_ids.len() - 1) as u64), 0) + }, + _ => panic!("unsupported!"), + }; + + ImageData::External( + ExternalImageData { + id: image_id, + channel_index: channel_idx, + image_type: ExternalImageType::TextureHandle(target) + } + ) + } +} + +impl ExternalImageHandler for LocalExternalImageHandler { + fn lock( + &mut self, + key: ExternalImageId, + _channel_index: u8, + ) -> ExternalImage { + let (id, desc) = self.texture_ids[key.0 as usize]; + ExternalImage { + uv: TexelRect::new(0.0, 0.0, desc.size.width as f32, desc.size.height as f32), + source: ExternalImageSource::NativeTexture(id), + } + } + fn unlock(&mut self, _key: ExternalImageId, _channel_index: u8) {} +} + +fn broadcast<T: Clone>(base_vals: &[T], num_items: usize) -> Vec<T> { + if base_vals.len() == num_items { + return base_vals.to_vec(); + } + + assert_eq!( + num_items % base_vals.len(), + 0, + "Cannot broadcast {} elements into {}", + base_vals.len(), + num_items + ); + + let mut vals = vec![]; + loop { + if vals.len() == num_items { + break; + } + vals.extend_from_slice(base_vals); + } + vals +} + +enum CheckerboardKind { + BlackGrey, + BlackTransparent, +} + +fn generate_checkerboard_image( + border: u32, + tile_x_size: u32, + tile_y_size: u32, + tile_x_count: u32, + tile_y_count: u32, + kind: CheckerboardKind, +) -> (ImageDescriptor, ImageData) { + let width = 2 * border + tile_x_size * tile_x_count; + let height = 2 * border + tile_y_size * tile_y_count; + let mut pixels = Vec::new(); + + for y in 0 .. height { + for x in 0 .. width { + if y < border || y >= (height - border) || + x < border || x >= (width - border) { + pixels.push(0); + pixels.push(0); + pixels.push(0xff); + pixels.push(0xff); + } else { + let xon = ((x - border) % (2 * tile_x_size)) < tile_x_size; + let yon = ((y - border) % (2 * tile_y_size)) < tile_y_size; + match kind { + CheckerboardKind::BlackGrey => { + let value = if xon ^ yon { 0xff } else { 0x7f }; + pixels.push(value); + pixels.push(value); + pixels.push(value); + pixels.push(0xff); + } + CheckerboardKind::BlackTransparent => { + let value = if xon ^ yon { 0xff } else { 0x00 }; + pixels.push(value); + pixels.push(value); + pixels.push(value); + pixels.push(value); + } + } + } + } + } + + let flags = match kind { + CheckerboardKind::BlackGrey => ImageDescriptorFlags::IS_OPAQUE, + CheckerboardKind::BlackTransparent => ImageDescriptorFlags::empty(), + }; + + ( + ImageDescriptor::new(width as i32, height as i32, ImageFormat::BGRA8, flags), + ImageData::new(pixels), + ) +} + +fn generate_xy_gradient_image(w: u32, h: u32) -> (ImageDescriptor, ImageData) { + let mut pixels = Vec::with_capacity((w * h * 4) as usize); + for y in 0 .. h { + for x in 0 .. w { + let grid = if x % 100 < 3 || y % 100 < 3 { 0.9 } else { 1.0 }; + pixels.push((y as f32 / h as f32 * 255.0 * grid) as u8); + pixels.push(0); + pixels.push((x as f32 / w as f32 * 255.0 * grid) as u8); + pixels.push(255); + } + } + + ( + ImageDescriptor::new(w as i32, h as i32, ImageFormat::BGRA8, ImageDescriptorFlags::IS_OPAQUE), + ImageData::new(pixels), + ) +} + +fn generate_solid_color_image( + r: u8, + g: u8, + b: u8, + a: u8, + w: u32, + h: u32, +) -> (ImageDescriptor, ImageData) { + let num_pixels: usize = (w * h).try_into().unwrap(); + let pixels = [b, g, r, a].repeat(num_pixels); + + let mut flags = ImageDescriptorFlags::empty(); + if a == 255 { + flags |= ImageDescriptorFlags::IS_OPAQUE; + } + + ( + ImageDescriptor::new(w as i32, h as i32, ImageFormat::BGRA8, flags), + ImageData::new(pixels), + ) +} + + + +fn is_image_opaque(format: ImageFormat, bytes: &[u8]) -> bool { + match format { + ImageFormat::BGRA8 | + ImageFormat::RGBA8 => { + let mut is_opaque = true; + for i in 0 .. (bytes.len() / 4) { + if bytes[i * 4 + 3] != 255 { + is_opaque = false; + break; + } + } + is_opaque + } + ImageFormat::RG8 => true, + ImageFormat::RG16 => true, + ImageFormat::R8 => false, + ImageFormat::R16 => false, + ImageFormat::RGBAF32 | + ImageFormat::RGBAI32 => unreachable!(), + } +} + +struct IsRoot(bool); + +pub struct YamlFrameReader { + yaml_path: PathBuf, + aux_dir: PathBuf, + frame_count: u32, + + display_lists: Vec<(PipelineId, BuiltDisplayList)>, + + watch_source: bool, + list_resources: bool, + + /// A HashMap of offsets which specify what scroll offsets particular + /// scroll layers should be initialized with. + scroll_offsets: HashMap<ExternalScrollId, Vec<SampledScrollOffset>>, + next_external_scroll_id: u64, + + image_map: HashMap<(PathBuf, Option<i64>), (ImageKey, LayoutSize)>, + + fonts: HashMap<FontDescriptor, FontKey>, + font_instances: HashMap<(FontKey, FontSize, FontInstanceFlags, Option<ColorU>, SyntheticItalics), FontInstanceKey>, + font_render_mode: Option<FontRenderMode>, + allow_mipmaps: bool, + + /// A HashMap that allows specifying a numeric id for clip and clip chains in YAML + /// and having each of those ids correspond to a unique ClipId. + user_clip_id_map: HashMap<u64, ClipId>, + user_clipchain_id_map: HashMap<u64, ClipChainId>, + user_spatial_id_map: HashMap<u64, SpatialId>, + + spatial_id_stack: Vec<SpatialId>, + + requested_frame: usize, + built_frame: usize, + + yaml_string: String, + keyframes: Option<Yaml>, + + external_image_handler: Option<Box<LocalExternalImageHandler>>, + + next_spatial_key: u64, +} + +impl YamlFrameReader { + pub fn new(yaml_path: &Path) -> YamlFrameReader { + YamlFrameReader { + watch_source: false, + list_resources: false, + yaml_path: yaml_path.to_owned(), + aux_dir: yaml_path.parent().unwrap().to_owned(), + frame_count: 0, + display_lists: Vec::new(), + scroll_offsets: HashMap::new(), + fonts: HashMap::new(), + font_instances: HashMap::new(), + font_render_mode: None, + allow_mipmaps: false, + image_map: HashMap::new(), + user_clip_id_map: HashMap::new(), + user_clipchain_id_map: HashMap::new(), + user_spatial_id_map: HashMap::new(), + spatial_id_stack: Vec::new(), + yaml_string: String::new(), + requested_frame: 0, + built_frame: usize::MAX, + keyframes: None, + external_image_handler: Some(Box::new(LocalExternalImageHandler::new())), + next_external_scroll_id: 1000, // arbitrary to easily see in logs which are implicit + next_spatial_key: 0, + } + } + + pub fn deinit(mut self, wrench: &mut Wrench) { + let mut txn = Transaction::new(); + + for (_, font_instance) in self.font_instances.drain() { + txn.delete_font_instance(font_instance); + } + + for (_, font) in self.fonts.drain() { + txn.delete_font(font); + } + + wrench.api.send_transaction(wrench.document_id, txn); + } + + fn top_space(&self) -> SpatialId { + *self.spatial_id_stack.last().unwrap() + } + + pub fn yaml_path(&self) -> &PathBuf { + &self.yaml_path + } + + pub fn new_from_args(args: &clap::ArgMatches) -> YamlFrameReader { + let yaml_file = args.value_of("INPUT").map(PathBuf::from).unwrap(); + + let mut y = YamlFrameReader::new(&yaml_file); + + y.keyframes = args.value_of("keyframes").map(|path| { + let mut file = File::open(&path).unwrap(); + let mut keyframes_string = String::new(); + file.read_to_string(&mut keyframes_string).unwrap(); + YamlLoader::load_from_str(&keyframes_string) + .expect("Failed to parse keyframes file") + .pop() + .unwrap() + }); + y.list_resources = args.is_present("list-resources"); + y.watch_source = args.is_present("watch"); + y + } + + pub fn reset(&mut self) { + self.scroll_offsets.clear(); + self.display_lists.clear(); + } + + fn build(&mut self, wrench: &mut Wrench) { + let yaml = YamlLoader::load_from_str(&self.yaml_string) + .map(|mut yaml| { + assert_eq!(yaml.len(), 1); + yaml.pop().unwrap() + }) + .expect("Failed to parse YAML file"); + + self.reset(); + + if let Some(pipelines) = yaml["pipelines"].as_vec() { + for pipeline in pipelines { + self.build_pipeline(wrench, pipeline["id"].as_pipeline_id().unwrap(), pipeline); + } + } + + let root_stacking_context = &yaml["root"]; + assert_ne!(*root_stacking_context, Yaml::BadValue); + self.build_pipeline(wrench, wrench.root_pipeline_id, root_stacking_context); + + // If replaying the same frame during interactive use, the frame gets rebuilt, + // but the external image handler has already been consumed by the renderer. + if let Some(external_image_handler) = self.external_image_handler.take() { + wrench.renderer.set_external_image_handler(external_image_handler); + } + } + + fn build_pipeline( + &mut self, + wrench: &mut Wrench, + pipeline_id: PipelineId, + yaml: &Yaml + ) { + // Don't allow referencing clips between pipelines for now. + self.user_clip_id_map.clear(); + self.user_clipchain_id_map.clear(); + self.user_spatial_id_map.clear(); + self.spatial_id_stack.clear(); + self.spatial_id_stack.push(SpatialId::root_scroll_node(pipeline_id)); + + let mut builder = DisplayListBuilder::new(pipeline_id); + builder.begin(); + let mut info = CommonItemProperties { + clip_rect: LayoutRect::zero(), + clip_chain_id: ClipChainId::INVALID, + spatial_id: SpatialId::new(0, PipelineId::dummy()), + flags: PrimitiveFlags::default(), + }; + self.add_stacking_context_from_yaml(&mut builder, wrench, yaml, IsRoot(true), &mut info); + self.display_lists.push(builder.end()); + + assert_eq!(self.spatial_id_stack.len(), 1); + } + + fn to_clip_chain_id( + &self, + item: &Yaml, + builder: &mut DisplayListBuilder, + ) -> Option<ClipChainId> { + match *item { + Yaml::Integer(value) => { + Some(self.user_clipchain_id_map[&(value as u64)]) + } + Yaml::Array(ref array) => { + let clip_ids: Vec<ClipId> = array + .iter() + .map(|id| { + let id = id.as_i64().expect("invalid clip id") as u64; + self.user_clip_id_map[&id] + }) + .collect(); + + Some(builder.define_clip_chain(None, clip_ids)) + } + _ => None, + } + } + + fn to_spatial_id(&self, item: &Yaml, pipeline_id: PipelineId) -> Option<SpatialId> { + match *item { + Yaml::Integer(value) => Some(self.user_spatial_id_map[&(value as u64)]), + Yaml::String(ref id_string) if id_string == "root-reference-frame" => + Some(SpatialId::root_reference_frame(pipeline_id)), + Yaml::String(ref id_string) if id_string == "root-scroll-node" => + Some(SpatialId::root_scroll_node(pipeline_id)), + Yaml::BadValue => None, + _ => { + println!("Unable to parse SpatialId {:?}", item); + None + } + } + } + + fn add_clip_id_mapping(&mut self, numeric_id: u64, real_id: ClipId) { + assert_ne!(numeric_id, 0, "id=0 is reserved for the root clip"); + self.user_clip_id_map.insert(numeric_id, real_id); + } + + fn add_clip_chain_id_mapping(&mut self, numeric_id: u64, real_id: ClipChainId) { + assert_ne!(numeric_id, 0, "id=0 is reserved for the root clip-chain"); + self.user_clipchain_id_map.insert(numeric_id, real_id); + } + + fn add_spatial_id_mapping(&mut self, numeric_id: u64, real_id: SpatialId) { + assert_ne!(numeric_id, 0, "id=0 is reserved for the root reference frame"); + assert_ne!(numeric_id, 1, "id=1 is reserved for the root scroll node"); + self.user_spatial_id_map.insert(numeric_id, real_id); + } + + fn to_hit_testing_tag(&self, item: &Yaml) -> Option<ItemTag> { + match *item { + Yaml::Array(ref array) if array.len() == 2 => { + match (array[0].as_i64(), array[1].as_i64()) { + (Some(first), Some(second)) => Some((first as u64, second as u16)), + _ => None, + } + } + _ => None, + } + + } + + fn add_or_get_image( + &mut self, + file: &Path, + tiling: Option<i64>, + item: &Yaml, + wrench: &mut Wrench, + ) -> (ImageKey, LayoutSize) { + let key = (file.to_owned(), tiling); + if let Some(k) = self.image_map.get(&key) { + return *k; + } + + if self.list_resources { println!("{}", file.to_string_lossy()); } + let (descriptor, image_data) = match image::open(file) { + Ok(image) => { + let (image_width, image_height) = image.dimensions(); + let (format, bytes) = match image { + image::DynamicImage::ImageLuma8(_) => { + (ImageFormat::R8, image.to_bytes()) + } + image::DynamicImage::ImageRgba8(_) => { + let mut pixels = image.to_bytes(); + premultiply(pixels.as_mut_slice()); + (ImageFormat::BGRA8, pixels) + } + image::DynamicImage::ImageRgb8(_) => { + let bytes = image.to_bytes(); + let mut pixels = Vec::with_capacity(image_width as usize * image_height as usize * 4); + for bgr in bytes.chunks(3) { + pixels.extend_from_slice(&[ + bgr[2], + bgr[1], + bgr[0], + 0xff + ]); + } + (ImageFormat::BGRA8, pixels) + } + _ => panic!("We don't support whatever your crazy image type is, come on"), + }; + let mut flags = ImageDescriptorFlags::empty(); + if is_image_opaque(format, &bytes[..]) { + flags |= ImageDescriptorFlags::IS_OPAQUE; + } + if self.allow_mipmaps { + flags |= ImageDescriptorFlags::ALLOW_MIPMAPS; + } + let descriptor = ImageDescriptor::new( + image_width as i32, + image_height as i32, + format, + flags, + ); + let data = ImageData::new(bytes); + (descriptor, data) + } + _ => { + // This is a hack but it is convenient when generating test cases and avoids + // bloating the repository. + match parse_function( + file.components() + .last() + .unwrap() + .as_os_str() + .to_str() + .unwrap(), + ) { + ("xy-gradient", args, _) => generate_xy_gradient_image( + args.get(0).unwrap_or(&"1000").parse::<u32>().unwrap(), + args.get(1).unwrap_or(&"1000").parse::<u32>().unwrap(), + ), + ("solid-color", args, _) => generate_solid_color_image( + args.get(0).unwrap_or(&"255").parse::<u8>().unwrap(), + args.get(1).unwrap_or(&"255").parse::<u8>().unwrap(), + args.get(2).unwrap_or(&"255").parse::<u8>().unwrap(), + args.get(3).unwrap_or(&"255").parse::<u8>().unwrap(), + args.get(4).unwrap_or(&"1000").parse::<u32>().unwrap(), + args.get(5).unwrap_or(&"1000").parse::<u32>().unwrap(), + ), + (name @ "transparent-checkerboard", args, _) | + (name @ "checkerboard", args, _) => { + let border = args.get(0).unwrap_or(&"4").parse::<u32>().unwrap(); + + let (x_size, y_size, x_count, y_count) = match args.len() { + 3 => { + let size = args.get(1).unwrap_or(&"32").parse::<u32>().unwrap(); + let count = args.get(2).unwrap_or(&"8").parse::<u32>().unwrap(); + (size, size, count, count) + } + 5 => { + let x_size = args.get(1).unwrap_or(&"32").parse::<u32>().unwrap(); + let y_size = args.get(2).unwrap_or(&"32").parse::<u32>().unwrap(); + let x_count = args.get(3).unwrap_or(&"8").parse::<u32>().unwrap(); + let y_count = args.get(4).unwrap_or(&"8").parse::<u32>().unwrap(); + (x_size, y_size, x_count, y_count) + } + _ => { + panic!("invalid checkerboard function"); + } + }; + + let kind = if name == "transparent-checkerboard" { + CheckerboardKind::BlackTransparent + } else { + CheckerboardKind::BlackGrey + }; + + generate_checkerboard_image( + border, + x_size, + y_size, + x_count, + y_count, + kind, + ) + } + _ => { + panic!("Failed to load image {:?}", file.to_str()); + } + } + } + }; + let tiling = tiling.map(|tile_size| tile_size as u16); + let image_key = wrench.api.generate_image_key(); + let mut txn = Transaction::new(); + + let external = item["external"].as_bool().unwrap_or(false); + if external { + // This indicates we want to simulate an external texture, + // ensure it gets created as such + let external_target = match item["external-target"].as_str() { + Some("2d") => ImageBufferKind::Texture2D, + Some("rect") => ImageBufferKind::TextureRect, + Some(t) => panic!("Unsupported external texture target: {}", t), + None => ImageBufferKind::Texture2D, + }; + + let external_image_data = + self.external_image_handler.as_mut().unwrap().add_image( + &wrench.renderer.device, + descriptor, + external_target, + image_data + ); + txn.add_image(image_key, descriptor, external_image_data, tiling); + } else { + txn.add_image(image_key, descriptor, image_data, tiling); + } + + wrench.api.send_transaction(wrench.document_id, txn); + let val = ( + image_key, + LayoutSize::new(descriptor.size.width as f32, descriptor.size.height as f32), + ); + self.image_map.insert(key, val); + val + } + + fn get_or_create_font(&mut self, desc: FontDescriptor, wrench: &mut Wrench) -> FontKey { + let list_resources = self.list_resources; + *self.fonts + .entry(desc.clone()) + .or_insert_with(|| match desc { + FontDescriptor::Path { + ref path, + font_index, + } => { + if list_resources { println!("{}", path.to_string_lossy()); } + let mut file = File::open(path).expect("Couldn't open font file"); + let mut bytes = vec![]; + file.read_to_end(&mut bytes) + .expect("failed to read font file"); + wrench.font_key_from_bytes(bytes, font_index) + } + FontDescriptor::Family { ref name } => wrench.font_key_from_name(name), + FontDescriptor::Properties { + ref family, + weight, + style, + stretch, + } => wrench.font_key_from_properties(family, weight, style, stretch), + }) + } + + pub fn allow_mipmaps(&mut self, allow_mipmaps: bool) { + self.allow_mipmaps = allow_mipmaps; + } + + pub fn set_font_render_mode(&mut self, render_mode: Option<FontRenderMode>) { + self.font_render_mode = render_mode; + } + + fn get_or_create_font_instance( + &mut self, + font_key: FontKey, + size: f32, + bg_color: Option<ColorU>, + flags: FontInstanceFlags, + synthetic_italics: SyntheticItalics, + wrench: &mut Wrench, + ) -> FontInstanceKey { + let font_render_mode = self.font_render_mode; + + *self.font_instances + .entry((font_key, size.into(), flags, bg_color, synthetic_italics)) + .or_insert_with(|| { + wrench.add_font_instance( + font_key, + size, + flags, + font_render_mode, + bg_color, + synthetic_italics, + ) + }) + } + + fn as_image_mask(&mut self, item: &Yaml, wrench: &mut Wrench) -> Option<ImageMask> { + item.as_hash()?; + + let tiling = item["tile-size"].as_i64(); + + let (image_key, image_dims) = match item["image"].as_str() { + Some("invalid") => (ImageKey::DUMMY, LayoutSize::new(100.0, 100.0)), + Some(filename) => { + let mut file = self.aux_dir.clone(); + file.push(filename); + self.add_or_get_image(&file, tiling, item, wrench) + } + None => { + warn!("No image provided for the image-mask!"); + return None; + } + }; + + let image_rect = item["rect"] + .as_rect() + .unwrap_or_else(|| LayoutRect::from_size(image_dims)); + Some(ImageMask { + image: image_key, + rect: image_rect, + }) + } + + fn handle_rect( + &self, + dl: &mut DisplayListBuilder, + item: &Yaml, + info: &CommonItemProperties, + ) { + let bounds_key = if item["type"].is_badvalue() { + "rect" + } else { + "bounds" + }; + + let bounds = self.resolve_rect(&item[bounds_key]); + let color = self.resolve_colorf(&item["color"]).unwrap_or(ColorF::BLACK); + dl.push_rect(info, bounds, color); + } + + fn handle_clear_rect( + &self, + dl: &mut DisplayListBuilder, + item: &Yaml, + info: &CommonItemProperties, + ) { + let bounds = item["bounds"].as_rect().expect("clear-rect type must have bounds"); + dl.push_clear_rect(info, bounds); + } + + fn handle_hit_test( + &mut self, + dl: &mut DisplayListBuilder, + item: &Yaml, + info: &mut CommonItemProperties, + ) { + info.clip_rect = try_intersect!( + item["bounds"].as_rect().expect("hit-test type must have bounds"), + &info.clip_rect + ); + + if let Some(tag) = self.to_hit_testing_tag(&item["hit-testing-tag"]) { + dl.push_hit_test( + info.clip_rect, + info.clip_chain_id, + info.spatial_id, + info.flags, + tag, + ); + } + } + + fn handle_line( + &mut self, + dl: &mut DisplayListBuilder, + item: &Yaml, + info: &mut CommonItemProperties, + ) { + let color = item["color"].as_colorf().unwrap_or(ColorF::BLACK); + let orientation = item["orientation"] + .as_str() + .and_then(LineOrientation::from_str) + .expect("line must have orientation"); + let style = item["style"] + .as_str() + .and_then(LineStyle::from_str) + .expect("line must have style"); + + let wavy_line_thickness = if let LineStyle::Wavy = style { + item["thickness"].as_f32().expect("wavy lines must have a thickness") + } else { + 0.0 + }; + + let area = if item["baseline"].is_badvalue() { + let bounds_key = if item["type"].is_badvalue() { + "rect" + } else { + "bounds" + }; + + item[bounds_key] + .as_rect() + .expect("line type must have bounds") + } else { + // Legacy line representation + let baseline = item["baseline"].as_f32().expect("line must have baseline"); + let start = item["start"].as_f32().expect("line must have start"); + let end = item["end"].as_f32().expect("line must have end"); + let width = item["width"].as_f32().expect("line must have width"); + + match orientation { + LineOrientation::Horizontal => { + LayoutRect::from_origin_and_size( + LayoutPoint::new(start, baseline), + LayoutSize::new(end - start, width), + ) + } + LineOrientation::Vertical => { + LayoutRect::from_origin_and_size( + LayoutPoint::new(baseline, start), + LayoutSize::new(width, end - start), + ) + } + } + }; + + dl.push_line( + info, + &area, + wavy_line_thickness, + orientation, + &color, + style, + ); + } + + fn handle_gradient( + &mut self, + dl: &mut DisplayListBuilder, + item: &Yaml, + info: &mut CommonItemProperties, + ) { + let bounds_key = if item["type"].is_badvalue() { + "gradient" + } else { + "bounds" + }; + let bounds = item[bounds_key] + .as_rect() + .expect("gradient must have bounds"); + + let gradient = item.as_gradient(dl); + let tile_size = item["tile-size"].as_size().unwrap_or_else(|| bounds.size()); + let tile_spacing = item["tile-spacing"].as_size().unwrap_or_else(LayoutSize::zero); + + dl.push_gradient( + info, + bounds, + gradient, + tile_size, + tile_spacing + ); + } + + fn handle_radial_gradient( + &mut self, + dl: &mut DisplayListBuilder, + item: &Yaml, + info: &mut CommonItemProperties, + ) { + let bounds_key = if item["type"].is_badvalue() { + "radial-gradient" + } else { + "bounds" + }; + let bounds = item[bounds_key] + .as_rect() + .expect("radial gradient must have bounds"); + let gradient = item.as_radial_gradient(dl); + let tile_size = item["tile-size"].as_size().unwrap_or_else(|| bounds.size()); + let tile_spacing = item["tile-spacing"].as_size().unwrap_or_else(LayoutSize::zero); + + dl.push_radial_gradient( + info, + bounds, + gradient, + tile_size, + tile_spacing, + ); + } + + fn handle_conic_gradient( + &mut self, + dl: &mut DisplayListBuilder, + item: &Yaml, + info: &mut CommonItemProperties, + ) { + let bounds_key = if item["type"].is_badvalue() { + "conic-gradient" + } else { + "bounds" + }; + let bounds = item[bounds_key] + .as_rect() + .expect("conic gradient must have bounds"); + let gradient = item.as_conic_gradient(dl); + let tile_size = item["tile-size"].as_size().unwrap_or_else(|| bounds.size()); + let tile_spacing = item["tile-spacing"].as_size().unwrap_or_else(LayoutSize::zero); + + dl.push_conic_gradient( + info, + bounds, + gradient, + tile_size, + tile_spacing, + ); + } + + fn handle_border( + &mut self, + dl: &mut DisplayListBuilder, + wrench: &mut Wrench, + item: &Yaml, + info: &mut CommonItemProperties, + ) { + let bounds_key = if item["type"].is_badvalue() { + "border" + } else { + "bounds" + }; + let bounds = item[bounds_key].as_rect().expect("borders must have bounds"); + let widths = item["width"] + .as_vec_f32() + .expect("borders must have width(s)"); + let widths = broadcast(&widths, 4); + let widths = LayoutSideOffsets::new(widths[0], widths[3], widths[2], widths[1]); + let border_details = if let Some(border_type) = item["border-type"].as_str() { + match border_type { + "normal" => { + let colors = item["color"] + .as_vec_colorf() + .expect("borders must have color(s)"); + let styles = item["style"] + .as_vec_string() + .expect("borders must have style(s)"); + let styles = styles + .iter() + .map(|s| match s.as_str() { + "none" => BorderStyle::None, + "solid" => BorderStyle::Solid, + "double" => BorderStyle::Double, + "dotted" => BorderStyle::Dotted, + "dashed" => BorderStyle::Dashed, + "hidden" => BorderStyle::Hidden, + "ridge" => BorderStyle::Ridge, + "inset" => BorderStyle::Inset, + "outset" => BorderStyle::Outset, + "groove" => BorderStyle::Groove, + s => { + panic!("Unknown border style '{}'", s); + } + }) + .collect::<Vec<BorderStyle>>(); + let radius = item["radius"] + .as_border_radius() + .unwrap_or_else(BorderRadius::zero); + + let colors = broadcast(&colors, 4); + let styles = broadcast(&styles, 4); + + let top = BorderSide { + color: colors[0], + style: styles[0], + }; + let right = BorderSide { + color: colors[1], + style: styles[1], + }; + let bottom = BorderSide { + color: colors[2], + style: styles[2], + }; + let left = BorderSide { + color: colors[3], + style: styles[3], + }; + let do_aa = item["do_aa"].as_bool().unwrap_or(true); + Some(BorderDetails::Normal(NormalBorder { + top, + left, + bottom, + right, + radius, + do_aa, + })) + } + "image" | "gradient" | "radial-gradient" | "conic-gradient" => { + let image_width = item["image-width"] + .as_i64() + .unwrap_or(bounds.width() as i64); + let image_height = item["image-height"] + .as_i64() + .unwrap_or(bounds.height() as i64); + let fill = item["fill"].as_bool().unwrap_or(false); + + let slice = if let Some(slice) = item["slice"].as_vec_u32() { + broadcast(&slice, 4) + } else { + vec![widths.top as u32, widths.left as u32, widths.bottom as u32, widths.right as u32] + }; + + let repeat_horizontal = match item["repeat-horizontal"] + .as_str() + .unwrap_or("stretch") + { + "stretch" => RepeatMode::Stretch, + "repeat" => RepeatMode::Repeat, + "round" => RepeatMode::Round, + "space" => RepeatMode::Space, + s => panic!("Unknown box border image repeat mode {}", s), + }; + let repeat_vertical = match item["repeat-vertical"] + .as_str() + .unwrap_or("stretch") + { + "stretch" => RepeatMode::Stretch, + "repeat" => RepeatMode::Repeat, + "round" => RepeatMode::Round, + "space" => RepeatMode::Space, + s => panic!("Unknown box border image repeat mode {}", s), + }; + let source = match border_type { + "image" => { + let file = rsrc_path(&item["image-source"], &self.aux_dir); + let (image_key, _) = self + .add_or_get_image(&file, None, item, wrench); + NinePatchBorderSource::Image(image_key, ImageRendering::Auto) + } + "gradient" => { + let gradient = item.as_gradient(dl); + NinePatchBorderSource::Gradient(gradient) + } + "radial-gradient" => { + let gradient = item.as_radial_gradient(dl); + NinePatchBorderSource::RadialGradient(gradient) + } + "conic-gradient" => { + let gradient = item.as_conic_gradient(dl); + NinePatchBorderSource::ConicGradient(gradient) + } + _ => unreachable!("Unexpected border type"), + }; + + Some(BorderDetails::NinePatch(NinePatchBorder { + source, + width: image_width as i32, + height: image_height as i32, + slice: SideOffsets2D::new(slice[0] as i32, slice[1] as i32, slice[2] as i32, slice[3] as i32), + fill, + repeat_horizontal, + repeat_vertical, + })) + } + _ => { + println!("Unable to parse border {:?}", item); + None + } + } + } else { + println!("Unable to parse border {:?}", item); + None + }; + if let Some(details) = border_details { + dl.push_border(info, bounds, widths, details); + } + } + + fn handle_box_shadow( + &mut self, + dl: &mut DisplayListBuilder, + item: &Yaml, + info: &mut CommonItemProperties, + ) { + let bounds_key = if item["type"].is_badvalue() { + "box-shadow" + } else { + "bounds" + }; + let bounds = item[bounds_key] + .as_rect() + .expect("box shadow must have bounds"); + let box_bounds = item["box-bounds"].as_rect().unwrap_or(bounds); + let offset = self.resolve_vector(&item["offset"], LayoutVector2D::zero()); + let color = item["color"] + .as_colorf() + .unwrap_or_else(|| ColorF::new(0.0, 0.0, 0.0, 1.0)); + let blur_radius = item["blur-radius"].as_force_f32().unwrap_or(0.0); + let spread_radius = item["spread-radius"].as_force_f32().unwrap_or(0.0); + let border_radius = item["border-radius"] + .as_border_radius() + .unwrap_or_else(BorderRadius::zero); + let clip_mode = if let Some(mode) = item["clip-mode"].as_str() { + match mode { + "outset" => BoxShadowClipMode::Outset, + "inset" => BoxShadowClipMode::Inset, + s => panic!("Unknown box shadow clip mode {}", s), + } + } else { + BoxShadowClipMode::Outset + }; + + dl.push_box_shadow( + info, + box_bounds, + offset, + color, + blur_radius, + spread_radius, + border_radius, + clip_mode, + ); + } + + fn handle_yuv_image( + &mut self, + dl: &mut DisplayListBuilder, + wrench: &mut Wrench, + item: &Yaml, + info: &mut CommonItemProperties, + ) { + // TODO(gw): Support other YUV color depth and spaces. + let color_depth = ColorDepth::Color8; + let color_space = YuvColorSpace::Rec709; + let color_range = ColorRange::Limited; + + let yuv_data = match item["format"].as_str().expect("no format supplied") { + "planar" => { + let y_path = rsrc_path(&item["src-y"], &self.aux_dir); + let (y_key, _) = self.add_or_get_image(&y_path, None, item, wrench); + + let u_path = rsrc_path(&item["src-u"], &self.aux_dir); + let (u_key, _) = self.add_or_get_image(&u_path, None, item, wrench); + + let v_path = rsrc_path(&item["src-v"], &self.aux_dir); + let (v_key, _) = self.add_or_get_image(&v_path, None, item, wrench); + + YuvData::PlanarYCbCr(y_key, u_key, v_key) + } + "nv12" => { + let y_path = rsrc_path(&item["src-y"], &self.aux_dir); + let (y_key, _) = self.add_or_get_image(&y_path, None, item, wrench); + + let uv_path = rsrc_path(&item["src-uv"], &self.aux_dir); + let (uv_key, _) = self.add_or_get_image(&uv_path, None, item, wrench); + + YuvData::NV12(y_key, uv_key) + } + "p010" => { + let y_path = rsrc_path(&item["src-y"], &self.aux_dir); + let (y_key, _) = self.add_or_get_image(&y_path, None, item, wrench); + + let uv_path = rsrc_path(&item["src-uv"], &self.aux_dir); + let (uv_key, _) = self.add_or_get_image(&uv_path, None, item, wrench); + + YuvData::P010(y_key, uv_key) + } + "interleaved" => { + let yuv_path = rsrc_path(&item["src"], &self.aux_dir); + let (yuv_key, _) = self.add_or_get_image(&yuv_path, None, item, wrench); + + YuvData::InterleavedYCbCr(yuv_key) + } + _ => { + panic!("unexpected yuv format"); + } + }; + + let bounds = item["bounds"].as_vec_f32().unwrap(); + let bounds = LayoutRect::from_origin_and_size( + LayoutPoint::new(bounds[0], bounds[1]), + LayoutSize::new(bounds[2], bounds[3]), + ); + + dl.push_yuv_image( + info, + bounds, + yuv_data, + color_depth, + color_space, + color_range, + ImageRendering::Auto, + ); + } + + fn handle_image( + &mut self, + dl: &mut DisplayListBuilder, + wrench: &mut Wrench, + item: &Yaml, + info: &mut CommonItemProperties, + ) { + let filename = &item[if item["type"].is_badvalue() { + "image" + } else { + "src" + }]; + let tiling = item["tile-size"].as_i64(); + let file = rsrc_path(filename, &self.aux_dir); + let (image_key, image_dims) = + self.add_or_get_image(&file, tiling, item, wrench); + + let bounds_raws = item["bounds"].as_vec_f32().unwrap(); + let bounds = if bounds_raws.len() == 2 { + LayoutRect::from_origin_and_size(LayoutPoint::new(bounds_raws[0], bounds_raws[1]), image_dims) + } else if bounds_raws.len() == 4 { + LayoutRect::from_origin_and_size( + LayoutPoint::new(bounds_raws[0], bounds_raws[1]), + LayoutSize::new(bounds_raws[2], bounds_raws[3]), + ) + } else { + panic!( + "image expected 2 or 4 values in bounds, got '{:?}'", + item["bounds"] + ); + }; + let rendering = match item["rendering"].as_str() { + Some("auto") | None => ImageRendering::Auto, + Some("crisp-edges") => ImageRendering::CrispEdges, + Some("pixelated") => ImageRendering::Pixelated, + Some(_) => panic!( + "ImageRendering can be auto, crisp-edges, or pixelated -- got {:?}", + item + ), + }; + let alpha_type = match item["alpha-type"].as_str() { + Some("premultiplied-alpha") | None => AlphaType::PremultipliedAlpha, + Some("alpha") => AlphaType::Alpha, + Some(_) => panic!( + "AlphaType can be premultiplied-alpha or alpha -- got {:?}", + item + ), + }; + let color = item["color"] + .as_colorf() + .unwrap_or_else(|| ColorF::WHITE); + let stretch_size = item["stretch-size"].as_size(); + let tile_spacing = item["tile-spacing"].as_size(); + if stretch_size.is_none() && tile_spacing.is_none() { + dl.push_image( + info, + bounds, + rendering, + alpha_type, + image_key, + color, + ); + } else { + dl.push_repeating_image( + info, + bounds, + stretch_size.unwrap_or(image_dims), + tile_spacing.unwrap_or_else(LayoutSize::zero), + rendering, + alpha_type, + image_key, + color, + ); + } + } + + fn handle_text( + &mut self, + dl: &mut DisplayListBuilder, + wrench: &mut Wrench, + item: &Yaml, + info: &mut CommonItemProperties, + ) { + let size = item["size"].as_pt_to_f32().unwrap_or(16.0); + let color = item["color"].as_colorf().unwrap_or(ColorF::BLACK); + let bg_color = item["bg-color"].as_colorf().map(|c| c.into()); + let synthetic_italics = if let Some(angle) = item["synthetic-italics"].as_f32() { + SyntheticItalics::from_degrees(angle) + } else if item["synthetic-italics"].as_bool().unwrap_or(false) { + SyntheticItalics::enabled() + } else { + SyntheticItalics::disabled() + }; + + let mut flags = FontInstanceFlags::empty(); + if item["synthetic-bold"].as_bool().unwrap_or(false) { + flags |= FontInstanceFlags::SYNTHETIC_BOLD; + } + if item["embedded-bitmaps"].as_bool().unwrap_or(false) { + flags |= FontInstanceFlags::EMBEDDED_BITMAPS; + } + if item["transpose"].as_bool().unwrap_or(false) { + flags |= FontInstanceFlags::TRANSPOSE; + } + if item["flip-x"].as_bool().unwrap_or(false) { + flags |= FontInstanceFlags::FLIP_X; + } + if item["flip-y"].as_bool().unwrap_or(false) { + flags |= FontInstanceFlags::FLIP_Y; + } + + assert!( + item["blur-radius"].is_badvalue(), + "text no longer has a blur radius, use PushShadow and PopAllShadows" + ); + + let desc = FontDescriptor::from_yaml(item, &self.aux_dir); + let font_key = self.get_or_create_font(desc, wrench); + let font_instance_key = self.get_or_create_font_instance(font_key, + size, + bg_color, + flags, + synthetic_italics, + wrench); + + assert!( + !(item["glyphs"].is_badvalue() && item["text"].is_badvalue()), + "text item had neither text nor glyphs!" + ); + + let (glyphs, rect) = if item["text"].is_badvalue() { + // if glyphs are specified, then the glyph positions can have the + // origin baked in. + let origin = item["origin"] + .as_point() + .unwrap_or(LayoutPoint::new(0.0, 0.0)); + let glyph_indices = item["glyphs"].as_vec_u32().unwrap(); + let glyph_offsets = item["offsets"].as_vec_f32().unwrap(); + assert_eq!(glyph_offsets.len(), glyph_indices.len() * 2); + + let glyphs = glyph_indices + .iter() + .enumerate() + .map(|k| { + GlyphInstance { + index: *k.1, + // In the future we want to change the API to be relative, eliminating this + point: LayoutPoint::new( + origin.x + glyph_offsets[k.0 * 2], + origin.y + glyph_offsets[k.0 * 2 + 1], + ), + } + }) + .collect::<Vec<_>>(); + // TODO(gw): We could optionally use the WR API to query glyph dimensions + // here and calculate the bounding region here if we want to. + let rect = item["bounds"] + .as_rect() + .expect("Text items with glyphs require bounds [for now]"); + (glyphs, rect) + } else { + let text = item["text"].as_str().unwrap(); + let origin = item["origin"] + .as_point() + .expect("origin required for text without glyphs"); + let (glyph_indices, glyph_positions, bounds) = wrench.layout_simple_ascii( + font_key, + font_instance_key, + text, + size, + origin, + flags, + ); + + let glyphs = glyph_indices + .iter() + .zip(glyph_positions) + .map(|arg| { + GlyphInstance { + index: *arg.0 as u32, + point: arg.1, + } + }) + .collect::<Vec<_>>(); + (glyphs, bounds) + }; + + dl.push_text( + info, + rect, + &glyphs, + font_instance_key, + color, + None, + ); + } + + fn handle_iframe( + &mut self, + dl: &mut DisplayListBuilder, + item: &Yaml, + info: &mut CommonItemProperties, + ) { + let bounds = item["bounds"].as_rect().expect("iframe must have bounds"); + let pipeline_id = item["id"].as_pipeline_id().unwrap(); + let ignore = item["ignore_missing_pipeline"].as_bool().unwrap_or(true); + dl.push_iframe( + bounds, + info.clip_rect, + &SpaceAndClipInfo { + spatial_id: info.spatial_id, + clip_chain_id: info.clip_chain_id + }, + pipeline_id, + ignore + ); + } + + fn get_item_type_from_yaml(item: &Yaml) -> &str { + let shorthands = [ + "rect", + "image", + "text", + "glyphs", + "box-shadow", // Note: box_shadow shorthand check has to come before border. + "border", + "gradient", + "radial-gradient", + "conic-gradient" + ]; + + for shorthand in shorthands.iter() { + if !item[*shorthand].is_badvalue() { + return shorthand; + } + } + item["type"].as_str().unwrap_or("unknown") + } + + fn add_display_list_items_from_yaml( + &mut self, + dl: &mut DisplayListBuilder, + wrench: &mut Wrench, + yaml_items: &[Yaml], + ) { + // A very large number (but safely far away from finite limits of f32) + let big_number = 1.0e30; + // A rect that should in practical terms serve as a no-op for clipping + let full_clip = LayoutRect::from_origin_and_size( + LayoutPoint::new(-big_number / 2.0, -big_number / 2.0), + LayoutSize::new(big_number, big_number)); + + for item in yaml_items { + let item_type = Self::get_item_type_from_yaml(item); + + let spatial_id = self.to_spatial_id(&item["spatial-id"], dl.pipeline_id); + + if let Some(spatial_id) = spatial_id { + self.spatial_id_stack.push(spatial_id); + } + + let clip_rect = item["clip-rect"].as_rect().unwrap_or(full_clip); + let clip_chain_id = self.to_clip_chain_id(&item["clip-chain"], dl).unwrap_or(ClipChainId::INVALID); + + let mut flags = PrimitiveFlags::default(); + for (key, flag) in [ + ("backface-visible", PrimitiveFlags::IS_BACKFACE_VISIBLE), + ("scrollbar-container", PrimitiveFlags::IS_SCROLLBAR_CONTAINER), + ("prefer-compositor-surface", PrimitiveFlags::PREFER_COMPOSITOR_SURFACE), + ] { + if let Some(value) = item[key].as_bool() { + flags.set(flag, value); + } + } + + + let mut info = CommonItemProperties { + clip_rect, + clip_chain_id, + spatial_id: self.top_space(), + flags, + }; + + match item_type { + "rect" => self.handle_rect(dl, item, &info), + "hit-test" => self.handle_hit_test(dl, item, &mut info), + "clear-rect" => self.handle_clear_rect(dl, item, &info), + "line" => self.handle_line(dl, item, &mut info), + "image" => self.handle_image(dl, wrench, item, &mut info), + "yuv-image" => self.handle_yuv_image(dl, wrench, item, &mut info), + "text" | "glyphs" => self.handle_text(dl, wrench, item, &mut info), + "scroll-frame" => self.handle_scroll_frame(dl, wrench, item), + "sticky-frame" => self.handle_sticky_frame(dl, wrench, item), + "clip" => self.handle_clip(dl, wrench, item), + "clip-chain" => self.handle_clip_chain(dl, item), + "border" => self.handle_border(dl, wrench, item, &mut info), + "gradient" => self.handle_gradient(dl, item, &mut info), + "radial-gradient" => self.handle_radial_gradient(dl, item, &mut info), + "conic-gradient" => self.handle_conic_gradient(dl, item, &mut info), + "box-shadow" => self.handle_box_shadow(dl, item, &mut info), + "iframe" => self.handle_iframe(dl, item, &mut info), + "stacking-context" => { + self.add_stacking_context_from_yaml(dl, wrench, item, IsRoot(false), &mut info) + } + "reference-frame" => self.handle_reference_frame(dl, wrench, item), + "computed-frame" => self.handle_computed_frame(dl, wrench, item), + "shadow" => self.handle_push_shadow(dl, item, &mut info), + "pop-all-shadows" => self.handle_pop_all_shadows(dl), + "backdrop-filter" => self.handle_backdrop_filter(dl, item, &mut info), + _ => println!("Skipping unknown item type: {:?}", item), + } + + if spatial_id.is_some() { + self.spatial_id_stack.pop().unwrap(); + } + } + } + + fn next_spatial_key(&mut self) -> SpatialTreeItemKey { + let key = SpatialTreeItemKey::new(self.next_spatial_key, 0); + self.next_spatial_key += 1; + key + } + + fn handle_scroll_frame( + &mut self, + dl: &mut DisplayListBuilder, + wrench: &mut Wrench, + yaml: &Yaml, + ) { + let clip_rect = yaml["bounds"] + .as_rect() + .expect("scroll frame must have a bounds"); + let content_size = yaml["content-size"].as_size().unwrap_or_else(|| clip_rect.size()); + let content_rect = LayoutRect::from_origin_and_size(clip_rect.min, content_size); + let external_scroll_offset = yaml["external-scroll-offset"].as_vector().unwrap_or_else(LayoutVector2D::zero); + let scroll_generation = yaml["scroll-generation"].as_i64().map_or(APZScrollGeneration::default(), |v| v as u64); + let has_scroll_linked_effect = + yaml["has-scroll-linked-effect"].as_bool().map_or(HasScrollLinkedEffect::default(), + |v| if v { HasScrollLinkedEffect::Yes } else { HasScrollLinkedEffect::No } + ); + + let numeric_id = yaml["id"].as_i64().map(|id| id as u64); + + let external_id = ExternalScrollId(self.next_external_scroll_id, dl.pipeline_id); + self.next_external_scroll_id += 1; + + if let Some(vector) = yaml["scroll-offset"].as_vector() { + self.scroll_offsets.insert( + external_id, + vec![SampledScrollOffset { + offset: vector, + generation: APZScrollGeneration::default(), + }], + ); + } + + if !yaml["scroll-offsets"].is_badvalue() { + let mut offsets = Vec::new(); + for entry in yaml["scroll-offsets"].as_vec().unwrap() { + let offset = entry["offset"].as_vector().unwrap_or(LayoutVector2D::zero()); + let generation = entry["generation"].as_i64().map_or(APZScrollGeneration::default(), |v| v as u64); + offsets.push(SampledScrollOffset { offset, generation }); + } + self.scroll_offsets.insert(external_id, offsets); + } + + let clip_to_frame = yaml["clip-to-frame"].as_bool().unwrap_or(false); + + let clip_id = if clip_to_frame { + Some(dl.define_clip_rect( + self.top_space(), + clip_rect, + )) + } else { + None + }; + + let spatial_id = dl.define_scroll_frame( + self.top_space(), + external_id, + content_rect, + clip_rect, + external_scroll_offset, + scroll_generation, + has_scroll_linked_effect, + self.next_spatial_key(), + ); + if let Some(numeric_id) = numeric_id { + self.add_spatial_id_mapping(numeric_id, spatial_id); + if let Some(clip_id) = clip_id { + self.add_clip_id_mapping(numeric_id, clip_id); + } + } + + if let Some(yaml_items) = yaml["items"].as_vec() { + self.spatial_id_stack.push(spatial_id); + self.add_display_list_items_from_yaml(dl, wrench, yaml_items); + self.spatial_id_stack.pop().unwrap(); + } + } + + fn handle_sticky_frame( + &mut self, + dl: &mut DisplayListBuilder, + wrench: &mut Wrench, + yaml: &Yaml, + ) { + let bounds = yaml["bounds"].as_rect().expect("sticky frame must have a bounds"); + let numeric_id = yaml["id"].as_i64().map(|id| id as u64); + + let real_id = dl.define_sticky_frame( + *self.spatial_id_stack.last().unwrap(), + bounds, + SideOffsets2D::new( + yaml["margin-top"].as_f32(), + yaml["margin-right"].as_f32(), + yaml["margin-bottom"].as_f32(), + yaml["margin-left"].as_f32(), + ), + yaml["vertical-offset-bounds"].as_sticky_offset_bounds(), + yaml["horizontal-offset-bounds"].as_sticky_offset_bounds(), + yaml["previously-applied-offset"].as_vector().unwrap_or_else(LayoutVector2D::zero), + self.next_spatial_key(), + ); + + if let Some(numeric_id) = numeric_id { + self.add_spatial_id_mapping(numeric_id, real_id); + } + + if let Some(yaml_items) = yaml["items"].as_vec() { + self.spatial_id_stack.push(real_id); + self.add_display_list_items_from_yaml(dl, wrench, yaml_items); + self.spatial_id_stack.pop().unwrap(); + } + } + + fn resolve_binding<'a>( + &'a self, + yaml: &'a Yaml, + ) -> &'a Yaml { + if let Some(keyframes) = &self.keyframes { + if let Some(s) = yaml.as_str() { + const PREFIX: &str = "key("; + const SUFFIX: &str = ")"; + if let Some(key) = s.strip_prefix(PREFIX).and_then(|s| s.strip_suffix(SUFFIX)) { + return &keyframes[key][self.requested_frame]; + } + } + } + + yaml + } + + fn resolve_colorf( + &self, + yaml: &Yaml, + ) -> Option<ColorF> { + self.resolve_binding(yaml) + .as_colorf() + } + + fn resolve_rect( + &self, + yaml: &Yaml, + ) -> LayoutRect { + self.resolve_binding(yaml) + .as_rect() + .unwrap_or_else(|| panic!("invalid rect {:?}", yaml)) + } + + fn resolve_vector( + &self, + yaml: &Yaml, + default: LayoutVector2D, + ) -> LayoutVector2D { + self.resolve_binding(yaml) + .as_vector() + .unwrap_or(default) + } + + fn handle_push_shadow( + &mut self, + dl: &mut DisplayListBuilder, + yaml: &Yaml, + info: &mut CommonItemProperties, + ) { + let blur_radius = yaml["blur-radius"].as_f32().unwrap_or(0.0); + let offset = yaml["offset"].as_vector().unwrap_or_else(LayoutVector2D::zero); + let color = yaml["color"].as_colorf().unwrap_or(ColorF::BLACK); + + dl.push_shadow( + &SpaceAndClipInfo { spatial_id: info.spatial_id, clip_chain_id: info.clip_chain_id }, + Shadow { + blur_radius, + offset, + color, + }, + true, + ); + } + + fn handle_pop_all_shadows(&mut self, dl: &mut DisplayListBuilder) { + dl.pop_all_shadows(); + } + + fn handle_clip_chain(&mut self, builder: &mut DisplayListBuilder, yaml: &Yaml) { + let numeric_id = yaml["id"].as_i64().expect("clip chains must have an id"); + let clip_ids: Vec<ClipId> = yaml["clips"] + .as_vec_u64() + .unwrap_or_default() + .iter() + .map(|id| self.user_clip_id_map[id]) + .collect(); + + let parent = self.to_clip_chain_id(&yaml["parent"], builder); + let real_id = builder.define_clip_chain(parent, clip_ids); + self.add_clip_chain_id_mapping(numeric_id as u64, real_id); + } + + fn handle_clip(&mut self, dl: &mut DisplayListBuilder, wrench: &mut Wrench, yaml: &Yaml) { + let numeric_id = yaml["id"].as_i64(); + let spatial_id = self.top_space(); + let complex_clips = yaml["complex"].as_complex_clip_regions(); + let mut clip_id = None; + + if let Some(clip_rect) = yaml["bounds"].as_rect() { + clip_id = Some(dl.define_clip_rect( + spatial_id, + clip_rect, + )); + } + + if let Some(image_mask) = self.as_image_mask(&yaml["image-mask"], wrench) { + assert!(clip_id.is_none(), "invalid clip definition"); + + clip_id = Some(dl.define_clip_image_mask( + spatial_id, + image_mask, + &[], + FillRule::Nonzero, + )); + } + + if !complex_clips.is_empty() { + // Only 1 complex clip is supported per clip (todo: change yaml format) + assert_eq!(complex_clips.len(), 1); + assert!(clip_id.is_none(), "invalid clip definition"); + + clip_id = Some(dl.define_clip_rounded_rect( + spatial_id, + complex_clips[0], + )); + } + + if let Some(clip_id) = clip_id { + if let Some(numeric_id) = numeric_id { + self.add_clip_id_mapping(numeric_id as u64, clip_id); + } + } + } + + fn push_reference_frame( + &mut self, + dl: &mut DisplayListBuilder, + default_bounds: impl Fn() -> LayoutRect, + yaml: &Yaml, + ) -> SpatialId { + let bounds = yaml["bounds"].as_rect().unwrap_or_else(default_bounds); + let default_transform_origin = LayoutPoint::new( + bounds.min.x + bounds.width() * 0.5, + bounds.min.y + bounds.height() * 0.5, + ); + + let transform_style = yaml["transform-style"] + .as_transform_style() + .unwrap_or(TransformStyle::Flat); + + let transform_origin = yaml["transform-origin"] + .as_point() + .unwrap_or(default_transform_origin); + + assert!( + yaml["transform"].is_badvalue() || + yaml["perspective"].is_badvalue(), + "Should have one of either transform or perspective" + ); + + let perspective_origin = yaml["perspective-origin"] + .as_point() + .unwrap_or(default_transform_origin); + + let reference_frame_kind = if !yaml["perspective"].is_badvalue() { + ReferenceFrameKind::Perspective { scrolling_relative_to: None } + } else { + ReferenceFrameKind::Transform { + is_2d_scale_translation: false, + should_snap: false, + paired_with_perspective: yaml["paired-with-perspective"].as_bool().unwrap_or(false), + } + }; + + let transform = yaml["transform"] + .as_transform(&transform_origin); + + let perspective = match yaml["perspective"].as_f32() { + Some(value) if value != 0.0 => { + Some(make_perspective(perspective_origin, value as f32)) + } + Some(..) => None, + _ => yaml["perspective"].as_matrix4d(), + }; + + let reference_frame_id = dl.push_reference_frame( + bounds.min, + *self.spatial_id_stack.last().unwrap(), + transform_style, + transform.or(perspective).unwrap_or_default().into(), + reference_frame_kind, + self.next_spatial_key(), + ); + + let numeric_id = yaml["id"].as_i64(); + if let Some(numeric_id) = numeric_id { + self.add_spatial_id_mapping(numeric_id as u64, reference_frame_id); + } + + reference_frame_id + } + + fn handle_reference_frame( + &mut self, + dl: &mut DisplayListBuilder, + wrench: &mut Wrench, + yaml: &Yaml, + ) { + let default_bounds = || LayoutRect::from_size(wrench.window_size_f32()); + let real_id = self.push_reference_frame(dl, default_bounds, yaml); + self.spatial_id_stack.push(real_id); + + if let Some(yaml_items) = yaml["items"].as_vec() { + self.add_display_list_items_from_yaml(dl, wrench, yaml_items); + } + + self.spatial_id_stack.pop().unwrap(); + dl.pop_reference_frame(); + } + + fn push_computed_frame( + &mut self, + dl: &mut DisplayListBuilder, + default_bounds: impl Fn() -> LayoutRect, + yaml: &Yaml, + ) -> SpatialId { + let bounds = yaml["bounds"].as_rect().unwrap_or_else(default_bounds); + + let scale_from = yaml["scale-from"].as_size(); + let vertical_flip = yaml["vertical-flip"].as_bool().unwrap_or(false); + let rotation = yaml["rotation"].as_rotation().unwrap_or(Rotation::Degree0); + + let reference_frame_id = dl.push_computed_frame( + bounds.min, + *self.spatial_id_stack.last().unwrap(), + scale_from, + vertical_flip, + rotation, + self.next_spatial_key(), + ); + + let numeric_id = yaml["id"].as_i64(); + if let Some(numeric_id) = numeric_id { + self.add_spatial_id_mapping(numeric_id as u64, reference_frame_id); + } + + reference_frame_id + } + + fn handle_computed_frame( + &mut self, + dl: &mut DisplayListBuilder, + wrench: &mut Wrench, + yaml: &Yaml, + ) { + let default_bounds = || LayoutRect::from_size(wrench.window_size_f32()); + let real_id = self.push_computed_frame(dl, default_bounds, yaml); + self.spatial_id_stack.push(real_id); + + if let Some(yaml_items) = yaml["items"].as_vec() { + self.add_display_list_items_from_yaml(dl, wrench, yaml_items); + } + + self.spatial_id_stack.pop().unwrap(); + dl.pop_reference_frame(); + } + + fn add_stacking_context_from_yaml( + &mut self, + dl: &mut DisplayListBuilder, + wrench: &mut Wrench, + yaml: &Yaml, + IsRoot(is_root): IsRoot, + info: &mut CommonItemProperties, + ) { + let default_bounds = || LayoutRect::from_size(wrench.window_size_f32()); + let mut bounds = yaml["bounds"].as_rect().unwrap_or_else(default_bounds); + + let pushed_reference_frame = + if !yaml["transform"].is_badvalue() || !yaml["perspective"].is_badvalue() { + let reference_frame_id = self.push_reference_frame(dl, default_bounds, yaml); + self.spatial_id_stack.push(reference_frame_id); + bounds.max -= bounds.min.to_vector(); + bounds.min = LayoutPoint::zero(); + true + } else { + false + }; + + let clip_chain_id = self.to_clip_chain_id(&yaml["clip-chain"], dl); + + let transform_style = yaml["transform-style"] + .as_transform_style() + .unwrap_or(TransformStyle::Flat); + let mix_blend_mode = yaml["mix-blend-mode"] + .as_mix_blend_mode() + .unwrap_or(MixBlendMode::Normal); + let raster_space = yaml["raster-space"] + .as_raster_space() + .unwrap_or(RasterSpace::Screen); + let is_blend_container = yaml["blend-container"].as_bool().unwrap_or(false); + let wraps_backdrop_filter = yaml["wraps-backdrop-filter"].as_bool().unwrap_or(false); + + if is_root { + if let Some(vector) = yaml["scroll-offset"].as_vector() { + let external_id = ExternalScrollId(0, dl.pipeline_id); + self.scroll_offsets.insert( + external_id, + vec![SampledScrollOffset { + offset: vector, + generation: APZScrollGeneration::default(), + }], + ); + } + } + + let filters = yaml["filters"].as_vec_filter_op().unwrap_or_default(); + let filter_datas = yaml["filter-datas"].as_vec_filter_data().unwrap_or_default(); + let filter_primitives = yaml["filter-primitives"].as_vec_filter_primitive().unwrap_or_default(); + + let mut flags = StackingContextFlags::empty(); + flags.set(StackingContextFlags::IS_BLEND_CONTAINER, is_blend_container); + flags.set(StackingContextFlags::WRAPS_BACKDROP_FILTER, wraps_backdrop_filter); + + dl.push_stacking_context( + bounds.min, + *self.spatial_id_stack.last().unwrap(), + info.flags, + clip_chain_id, + transform_style, + mix_blend_mode, + &filters, + &filter_datas, + &filter_primitives, + raster_space, + flags, + ); + + if let Some(yaml_items) = yaml["items"].as_vec() { + self.add_display_list_items_from_yaml(dl, wrench, yaml_items); + } + + dl.pop_stacking_context(); + + if pushed_reference_frame { + self.spatial_id_stack.pop().unwrap(); + dl.pop_reference_frame(); + } + } + + fn handle_backdrop_filter( + &mut self, + dl: &mut DisplayListBuilder, + item: &Yaml, + info: &mut CommonItemProperties, + ) { + info.clip_rect = try_intersect!( + self.resolve_rect(&item["bounds"]), + &info.clip_rect + ); + + let filters = item["filters"].as_vec_filter_op().unwrap_or_default(); + let filter_datas = item["filter-datas"].as_vec_filter_data().unwrap_or_default(); + let filter_primitives = item["filter-primitives"].as_vec_filter_primitive().unwrap_or_default(); + + dl.push_backdrop_filter( + info, + &filters, + &filter_datas, + &filter_primitives, + ); + } +} + +impl WrenchThing for YamlFrameReader { + fn do_frame(&mut self, wrench: &mut Wrench) -> u32 { + let mut should_build_yaml = false; + + // If YAML isn't read yet, or watching source file, reload from disk. + if self.yaml_string.is_empty() || self.watch_source { + self.yaml_string = std::fs::read_to_string(&self.yaml_path) + .unwrap_or_else(|_| panic!("YAML '{:?}' doesn't exist", self.yaml_path)); + should_build_yaml = true; + } + + // Evaluate conditions that require parsing the YAML. + if self.built_frame != self.requested_frame { + // Requested frame has changed + should_build_yaml = true; + } + + // Build the DL from YAML if required + if should_build_yaml { + self.build(wrench); + } + + // Determine whether to send a new DL, or just refresh. + if should_build_yaml || wrench.should_rebuild_display_lists() { + wrench.begin_frame(); + wrench.send_lists( + self.frame_count, + self.display_lists.clone(), + &self.scroll_offsets, + ); + } else { + wrench.refresh(); + } + + self.frame_count += 1; + self.frame_count + } + + fn next_frame(&mut self) { + let mut max_frame_count = 0; + if let Some(ref keyframes) = self.keyframes { + for (_, values) in keyframes.as_hash().unwrap() { + max_frame_count = max_frame_count.max(values.as_vec().unwrap().len()); + } + } + if self.requested_frame + 1 < max_frame_count { + self.requested_frame += 1; + } + } + + fn prev_frame(&mut self) { + if self.requested_frame > 0 { + self.requested_frame -= 1; + } + } +} diff --git a/gfx/wr/wrench/src/yaml_helper.rs b/gfx/wr/wrench/src/yaml_helper.rs new file mode 100644 index 0000000000..c28fad04ce --- /dev/null +++ b/gfx/wr/wrench/src/yaml_helper.rs @@ -0,0 +1,923 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use euclid::{Angle, Size2D}; +use crate::parse_function::parse_function; +use std::f32; +use std::str::FromStr; +use webrender::api::*; +use webrender::api::units::*; +use yaml_rust::{Yaml, YamlLoader}; + +pub trait YamlHelper { + fn as_f32(&self) -> Option<f32>; + fn as_force_f32(&self) -> Option<f32>; + fn as_vec_f32(&self) -> Option<Vec<f32>>; + fn as_vec_u32(&self) -> Option<Vec<u32>>; + fn as_vec_u64(&self) -> Option<Vec<u64>>; + fn as_pipeline_id(&self) -> Option<PipelineId>; + fn as_rect(&self) -> Option<LayoutRect>; + fn as_size(&self) -> Option<LayoutSize>; + fn as_point(&self) -> Option<LayoutPoint>; + fn as_vector(&self) -> Option<LayoutVector2D>; + fn as_matrix4d(&self) -> Option<LayoutTransform>; + fn as_transform(&self, transform_origin: &LayoutPoint) -> Option<LayoutTransform>; + fn as_colorf(&self) -> Option<ColorF>; + fn as_vec_colorf(&self) -> Option<Vec<ColorF>>; + fn as_px_to_f32(&self) -> Option<f32>; + fn as_pt_to_f32(&self) -> Option<f32>; + fn as_vec_string(&self) -> Option<Vec<String>>; + fn as_border_radius_component(&self) -> LayoutSize; + fn as_border_radius(&self) -> Option<BorderRadius>; + fn as_transform_style(&self) -> Option<TransformStyle>; + fn as_raster_space(&self) -> Option<RasterSpace>; + fn as_clip_mode(&self) -> Option<ClipMode>; + fn as_mix_blend_mode(&self) -> Option<MixBlendMode>; + fn as_filter_op(&self) -> Option<FilterOp>; + fn as_vec_filter_op(&self) -> Option<Vec<FilterOp>>; + fn as_filter_data(&self) -> Option<FilterData>; + fn as_vec_filter_data(&self) -> Option<Vec<FilterData>>; + fn as_filter_input(&self) -> Option<FilterPrimitiveInput>; + fn as_filter_primitive(&self) -> Option<FilterPrimitive>; + fn as_vec_filter_primitive(&self) -> Option<Vec<FilterPrimitive>>; + fn as_color_space(&self) -> Option<ColorSpace>; + fn as_complex_clip_region(&self) -> ComplexClipRegion; + fn as_sticky_offset_bounds(&self) -> StickyOffsetBounds; + fn as_gradient(&self, dl: &mut DisplayListBuilder) -> Gradient; + fn as_radial_gradient(&self, dl: &mut DisplayListBuilder) -> RadialGradient; + fn as_conic_gradient(&self, dl: &mut DisplayListBuilder) -> ConicGradient; + fn as_complex_clip_regions(&self) -> Vec<ComplexClipRegion>; + fn as_rotation(&self) -> Option<Rotation>; +} + +fn string_to_color(color: &str) -> Option<ColorF> { + match color { + "red" => Some(ColorF::new(1.0, 0.0, 0.0, 1.0)), + "green" => Some(ColorF::new(0.0, 1.0, 0.0, 1.0)), + "blue" => Some(ColorF::new(0.0, 0.0, 1.0, 1.0)), + "white" => Some(ColorF::new(1.0, 1.0, 1.0, 1.0)), + "black" => Some(ColorF::new(0.0, 0.0, 0.0, 1.0)), + "yellow" => Some(ColorF::new(1.0, 1.0, 0.0, 1.0)), + "cyan" => Some(ColorF::new(0.0, 1.0, 1.0, 1.0)), + "magenta" => Some(ColorF::new(1.0, 0.0, 1.0, 1.0)), + "transparent" => Some(ColorF::new(1.0, 1.0, 1.0, 0.0)), + s => { + let items: Vec<f32> = s.split_whitespace() + .map(|s| f32::from_str(s).unwrap()) + .collect(); + if items.len() == 3 { + Some(ColorF::new( + items[0] / 255.0, + items[1] / 255.0, + items[2] / 255.0, + 1.0, + )) + } else if items.len() == 4 { + Some(ColorF::new( + items[0] / 255.0, + items[1] / 255.0, + items[2] / 255.0, + items[3], + )) + } else { + None + } + } + } +} + +pub trait StringEnum: Sized { + fn from_str(_: &str) -> Option<Self>; + fn as_str(&self) -> &'static str; +} + +macro_rules! define_string_enum { + ($T:ident, [ $( $y:ident = $x:expr ),* ]) => { + impl StringEnum for $T { + fn from_str(text: &str) -> Option<$T> { + match text { + $( $x => Some($T::$y), )* + _ => { + println!("Unrecognized {} value '{}'", stringify!($T), text); + None + } + } + } + fn as_str(&self) -> &'static str { + match *self { + $( $T::$y => $x, )* + } + } + } + } +} + +define_string_enum!(TransformStyle, [Flat = "flat", Preserve3D = "preserve-3d"]); + +define_string_enum!( + MixBlendMode, + [ + Normal = "normal", + Multiply = "multiply", + Screen = "screen", + Overlay = "overlay", + Darken = "darken", + Lighten = "lighten", + ColorDodge = "color-dodge", + ColorBurn = "color-burn", + HardLight = "hard-light", + SoftLight = "soft-light", + Difference = "difference", + Exclusion = "exclusion", + Hue = "hue", + Saturation = "saturation", + Color = "color", + Luminosity = "luminosity", + PlusLighter = "plus-lighter" + ] +); + +define_string_enum!( + LineOrientation, + [Horizontal = "horizontal", Vertical = "vertical"] +); + +define_string_enum!( + LineStyle, + [ + Solid = "solid", + Dotted = "dotted", + Dashed = "dashed", + Wavy = "wavy" + ] +); + +define_string_enum!(ClipMode, [Clip = "clip", ClipOut = "clip-out"]); + +define_string_enum!( + ComponentTransferFuncType, + [ + Identity = "Identity", + Table = "Table", + Discrete = "Discrete", + Linear = "Linear", + Gamma = "Gamma" + ] +); + +define_string_enum!( + ColorSpace, + [ + Srgb = "srgb", + LinearRgb = "linear-rgb" + ] +); + +// Rotate around `axis` by `degrees` angle +fn make_rotation( + origin: &LayoutPoint, + degrees: f32, + axis_x: f32, + axis_y: f32, + axis_z: f32, +) -> LayoutTransform { + let pre_transform = LayoutTransform::translation(-origin.x, -origin.y, -0.0); + let post_transform = LayoutTransform::translation(origin.x, origin.y, 0.0); + + let theta = 2.0f32 * f32::consts::PI - degrees.to_radians(); + let transform = + LayoutTransform::identity().pre_rotate(axis_x, axis_y, axis_z, Angle::radians(theta)); + + pre_transform.then(&transform).then(&post_transform) +} + +pub fn make_perspective( + origin: LayoutPoint, + perspective: f32, +) -> LayoutTransform { + let pre_transform = LayoutTransform::translation(-origin.x, -origin.y, -0.0); + let post_transform = LayoutTransform::translation(origin.x, origin.y, 0.0); + let transform = LayoutTransform::perspective(perspective); + pre_transform.then(&transform).then(&post_transform) +} + +// Create a skew matrix, specified in degrees. +fn make_skew( + skew_x: f32, + skew_y: f32, +) -> LayoutTransform { + let alpha = Angle::radians(skew_x.to_radians()); + let beta = Angle::radians(skew_y.to_radians()); + LayoutTransform::skew(alpha, beta) +} + +impl YamlHelper for Yaml { + fn as_f32(&self) -> Option<f32> { + match *self { + Yaml::Integer(iv) => Some(iv as f32), + Yaml::Real(ref sv) => f32::from_str(sv.as_str()).ok(), + _ => None, + } + } + + fn as_force_f32(&self) -> Option<f32> { + match *self { + Yaml::Integer(iv) => Some(iv as f32), + Yaml::String(ref sv) | Yaml::Real(ref sv) => f32::from_str(sv.as_str()).ok(), + _ => None, + } + } + + fn as_vec_f32(&self) -> Option<Vec<f32>> { + match *self { + Yaml::String(ref s) | Yaml::Real(ref s) => s.split_whitespace() + .map(f32::from_str) + .collect::<Result<Vec<_>, _>>() + .ok(), + Yaml::Array(ref v) => v.iter() + .map(|v| match *v { + Yaml::Integer(k) => Ok(k as f32), + Yaml::String(ref k) | Yaml::Real(ref k) => f32::from_str(k).map_err(|_| false), + _ => Err(false), + }) + .collect::<Result<Vec<_>, _>>() + .ok(), + Yaml::Integer(k) => Some(vec![k as f32]), + _ => None, + } + } + + fn as_vec_u32(&self) -> Option<Vec<u32>> { + self.as_vec().map(|v| v.iter().map(|v| v.as_i64().unwrap() as u32).collect()) + } + + fn as_vec_u64(&self) -> Option<Vec<u64>> { + self.as_vec().map(|v| v.iter().map(|v| v.as_i64().unwrap() as u64).collect()) + } + + fn as_pipeline_id(&self) -> Option<PipelineId> { + if let Some(v) = self.as_vec() { + let a = v.get(0).and_then(|v| v.as_i64()).map(|v| v as u32); + let b = v.get(1).and_then(|v| v.as_i64()).map(|v| v as u32); + match (a, b) { + (Some(a), Some(b)) if v.len() == 2 => Some(PipelineId(a, b)), + _ => None, + } + } else { + None + } + } + + fn as_px_to_f32(&self) -> Option<f32> { + self.as_force_f32() + } + + fn as_pt_to_f32(&self) -> Option<f32> { + self.as_force_f32().map(|fv| fv * 16. / 12.) + } + + fn as_rect(&self) -> Option<LayoutRect> { + self.as_vec_f32().and_then(|v| match v.as_slice() { + &[x, y, width, height] => Some(LayoutRect::from_origin_and_size( + LayoutPoint::new(x, y), + LayoutSize::new(width, height), + )), + _ => None, + }) + } + + fn as_size(&self) -> Option<LayoutSize> { + if self.is_badvalue() { + return None; + } + + if let Some(nums) = self.as_vec_f32() { + if nums.len() == 2 { + return Some(LayoutSize::new(nums[0], nums[1])); + } + } + + None + } + + fn as_point(&self) -> Option<LayoutPoint> { + if self.is_badvalue() { + return None; + } + + if let Some(nums) = self.as_vec_f32() { + if nums.len() == 2 { + return Some(LayoutPoint::new(nums[0], nums[1])); + } + } + + None + } + + fn as_vector(&self) -> Option<LayoutVector2D> { + self.as_point().map(|p| p.to_vector()) + } + + fn as_matrix4d(&self) -> Option<LayoutTransform> { + if let Some(nums) = self.as_vec_f32() { + assert_eq!(nums.len(), 16, "expected 16 floats, got '{:?}'", self); + Some(LayoutTransform::new( + nums[0], nums[1], nums[2], nums[3], + nums[4], nums[5], nums[6], nums[7], + nums[8], nums[9], nums[10], nums[11], + nums[12], nums[13], nums[14], nums[15], + )) + } else { + None + } + } + + fn as_transform(&self, transform_origin: &LayoutPoint) -> Option<LayoutTransform> { + if let Some(transform) = self.as_matrix4d() { + return Some(transform); + } + + match *self { + Yaml::String(ref string) => { + let mut slice = string.as_str(); + let mut transform = LayoutTransform::identity(); + while !slice.is_empty() { + let (function, ref args, reminder) = parse_function(slice); + slice = reminder; + let mx = match function { + "translate" if args.len() >= 2 => { + let z = args.get(2).and_then(|a| a.parse().ok()).unwrap_or(0.); + LayoutTransform::translation( + args[0].parse().unwrap(), + args[1].parse().unwrap(), + z, + ) + } + "rotate" | "rotate-z" if args.len() == 1 => { + make_rotation(transform_origin, args[0].parse().unwrap(), 0.0, 0.0, 1.0) + } + "rotate-x" if args.len() == 1 => { + make_rotation(transform_origin, args[0].parse().unwrap(), 1.0, 0.0, 0.0) + } + "rotate-y" if args.len() == 1 => { + make_rotation(transform_origin, args[0].parse().unwrap(), 0.0, 1.0, 0.0) + } + "scale" if !args.is_empty() => { + let x = args[0].parse().unwrap(); + // Default to uniform X/Y scale if Y unspecified. + let y = args.get(1).and_then(|a| a.parse().ok()).unwrap_or(x); + // Default to no Z scale if unspecified. + let z = args.get(2).and_then(|a| a.parse().ok()).unwrap_or(1.0); + LayoutTransform::scale(x, y, z) + } + "scale-x" if args.len() == 1 => { + LayoutTransform::scale(args[0].parse().unwrap(), 1.0, 1.0) + } + "scale-y" if args.len() == 1 => { + LayoutTransform::scale(1.0, args[0].parse().unwrap(), 1.0) + } + "scale-z" if args.len() == 1 => { + LayoutTransform::scale(1.0, 1.0, args[0].parse().unwrap()) + } + "skew" if !args.is_empty() => { + // Default to no Y skew if unspecified. + let skew_y = args.get(1).and_then(|a| a.parse().ok()).unwrap_or(0.0); + make_skew(args[0].parse().unwrap(), skew_y) + } + "skew-x" if args.len() == 1 => { + make_skew(args[0].parse().unwrap(), 0.0) + } + "skew-y" if args.len() == 1 => { + make_skew(0.0, args[0].parse().unwrap()) + } + "perspective" if args.len() == 1 => { + LayoutTransform::perspective(args[0].parse().unwrap()) + } + _ => { + println!("unknown function {}", function); + break; + } + }; + transform = transform.then(&mx); + } + Some(transform) + } + Yaml::Array(ref array) => { + let transform = array.iter().fold( + LayoutTransform::identity(), + |u, yaml| if let Some(transform) = yaml.as_transform(transform_origin) { + transform.then(&u) + } else { + u + }, + ); + Some(transform) + } + Yaml::BadValue => None, + _ => { + println!("unknown transform {:?}", self); + None + } + } + } + + /// Inputs for r, g, b channels are floats or ints in the range [0, 255]. + /// If included, the alpha channel is in the range [0, 1]. + /// This matches CSS-style, but requires conversion for `ColorF`. + fn as_colorf(&self) -> Option<ColorF> { + if let Some(nums) = self.as_vec_f32() { + assert!(nums.iter().take(3).all(|x| (0.0 ..= 255.0).contains(x)), + "r, g, b values should be in the 0-255 range, got {:?}", nums); + + let color: ColorF = match *nums.as_slice() { + [r, g, b] => ColorF { r, g, b, a: 1.0 }, + [r, g, b, a] => ColorF { r, g, b, a }, + _ => panic!("color expected a color name, or 3-4 floats; got '{:?}'", self), + }.scale_rgb(1.0 / 255.0); + + assert!((0.0 ..= 1.0).contains(&color.a), + "alpha value should be in the 0-1 range, got {:?}", + color.a); + + Some(color) + } else if let Some(s) = self.as_str() { + string_to_color(s) + } else { + None + } + } + + fn as_vec_colorf(&self) -> Option<Vec<ColorF>> { + if let Some(v) = self.as_vec() { + Some(v.iter().map(|v| v.as_colorf().unwrap()).collect()) + } else { self.as_colorf().map(|color| vec![color]) } + } + + fn as_vec_string(&self) -> Option<Vec<String>> { + if let Some(v) = self.as_vec() { + Some(v.iter().map(|v| v.as_str().unwrap().to_owned()).collect()) + } else { self.as_str().map(|s| vec![s.to_owned()]) } + } + + fn as_border_radius_component(&self) -> LayoutSize { + if let Yaml::Integer(integer) = *self { + return LayoutSize::new(integer as f32, integer as f32); + } + self.as_size().unwrap_or_else(Size2D::zero) + } + + fn as_border_radius(&self) -> Option<BorderRadius> { + if let Some(size) = self.as_size() { + return Some(BorderRadius::uniform_size(size)); + } + + match *self { + Yaml::BadValue => None, + Yaml::String(ref s) | Yaml::Real(ref s) => { + let fv = f32::from_str(s).unwrap(); + Some(BorderRadius::uniform(fv)) + } + Yaml::Integer(v) => Some(BorderRadius::uniform(v as f32)), + Yaml::Array(ref array) if array.len() == 4 => { + let top_left = array[0].as_border_radius_component(); + let top_right = array[1].as_border_radius_component(); + let bottom_left = array[2].as_border_radius_component(); + let bottom_right = array[3].as_border_radius_component(); + Some(BorderRadius { + top_left, + top_right, + bottom_left, + bottom_right, + }) + } + Yaml::Hash(_) => { + let top_left = self["top-left"].as_border_radius_component(); + let top_right = self["top-right"].as_border_radius_component(); + let bottom_left = self["bottom-left"].as_border_radius_component(); + let bottom_right = self["bottom-right"].as_border_radius_component(); + Some(BorderRadius { + top_left, + top_right, + bottom_left, + bottom_right, + }) + } + _ => { + panic!("Invalid border radius specified: {:?}", self); + } + } + } + + fn as_transform_style(&self) -> Option<TransformStyle> { + self.as_str().and_then(StringEnum::from_str) + } + + fn as_raster_space(&self) -> Option<RasterSpace> { + self.as_str().map(|s| { + match parse_function(s) { + ("screen", _, _) => { + RasterSpace::Screen + } + ("local", ref args, _) if args.len() == 1 => { + RasterSpace::Local(args[0].parse().unwrap()) + } + f => { + panic!("error parsing raster space {:?}", f); + } + } + }) + } + + fn as_mix_blend_mode(&self) -> Option<MixBlendMode> { + self.as_str().and_then(StringEnum::from_str) + } + + fn as_clip_mode(&self) -> Option<ClipMode> { + self.as_str().and_then(StringEnum::from_str) + } + + fn as_filter_op(&self) -> Option<FilterOp> { + if let Some(s) = self.as_str() { + match parse_function(s) { + ("identity", _, _) => { + Some(FilterOp::Identity) + } + ("component-transfer", _, _) => { + Some(FilterOp::ComponentTransfer) + } + ("blur", ref args, _) if args.len() == 2 => { + Some(FilterOp::Blur(args[0].parse().unwrap(), args[1].parse().unwrap())) + } + ("brightness", ref args, _) if args.len() == 1 => { + Some(FilterOp::Brightness(args[0].parse().unwrap())) + } + ("contrast", ref args, _) if args.len() == 1 => { + Some(FilterOp::Contrast(args[0].parse().unwrap())) + } + ("grayscale", ref args, _) if args.len() == 1 => { + Some(FilterOp::Grayscale(args[0].parse().unwrap())) + } + ("hue-rotate", ref args, _) if args.len() == 1 => { + Some(FilterOp::HueRotate(args[0].parse().unwrap())) + } + ("invert", ref args, _) if args.len() == 1 => { + Some(FilterOp::Invert(args[0].parse().unwrap())) + } + ("opacity", ref args, _) if args.len() == 1 => { + let amount: f32 = args[0].parse().unwrap(); + Some(FilterOp::Opacity(amount.into(), amount)) + } + ("saturate", ref args, _) if args.len() == 1 => { + Some(FilterOp::Saturate(args[0].parse().unwrap())) + } + ("sepia", ref args, _) if args.len() == 1 => { + Some(FilterOp::Sepia(args[0].parse().unwrap())) + } + ("srgb-to-linear", _, _) => Some(FilterOp::SrgbToLinear), + ("linear-to-srgb", _, _) => Some(FilterOp::LinearToSrgb), + ("drop-shadow", ref args, _) if args.len() == 3 => { + let str = format!("---\noffset: {}\nblur-radius: {}\ncolor: {}\n", args[0], args[1], args[2]); + let mut yaml_doc = YamlLoader::load_from_str(&str).expect("Failed to parse drop-shadow"); + let yaml = yaml_doc.pop().unwrap(); + Some(FilterOp::DropShadow(Shadow { + offset: yaml["offset"].as_vector().unwrap(), + blur_radius: yaml["blur-radius"].as_f32().unwrap(), + color: yaml["color"].as_colorf().unwrap() + })) + } + ("color-matrix", ref args, _) if args.len() == 20 => { + let m: Vec<f32> = args.iter().map(|f| f.parse().unwrap()).collect(); + let mut matrix: [f32; 20] = [0.0; 20]; + matrix.clone_from_slice(&m); + Some(FilterOp::ColorMatrix(matrix)) + } + ("flood", ref args, _) if args.len() == 1 => { + let str = format!("---\ncolor: {}\n", args[0]); + let mut yaml_doc = YamlLoader::load_from_str(&str).expect("Failed to parse flood"); + let yaml = yaml_doc.pop().unwrap(); + Some(FilterOp::Flood(yaml["color"].as_colorf().unwrap())) + } + (_, _, _) => None, + } + } else { + None + } + } + + fn as_vec_filter_op(&self) -> Option<Vec<FilterOp>> { + if let Some(v) = self.as_vec() { + Some(v.iter().map(|x| x.as_filter_op().unwrap()).collect()) + } else { + self.as_filter_op().map(|op| vec![op]) + } + } + + fn as_filter_data(&self) -> Option<FilterData> { + // Parse an array with five entries. First entry is an array of func types (4). + // The remaining entries are arrays of floats. + if let Yaml::Array(ref array) = *self { + if array.len() != 5 { + panic!("Invalid filter data specified, base array doesn't have five entries: {:?}", self); + } + if let Some(func_types_p) = array[0].as_vec_string() { + if func_types_p.len() != 4 { + panic!("Invalid filter data specified, func type array doesn't have five entries: {:?}", self); + } + let func_types: Vec<ComponentTransferFuncType> = + func_types_p.into_iter().map(|x| + StringEnum::from_str(&x).unwrap_or_else(|| + panic!("Invalid filter data specified, invalid func type name: {:?}", self)) + ).collect(); + if let Some(r_values_p) = array[1].as_vec_f32() { + if let Some(g_values_p) = array[2].as_vec_f32() { + if let Some(b_values_p) = array[3].as_vec_f32() { + if let Some(a_values_p) = array[4].as_vec_f32() { + let filter_data = FilterData { + func_r_type: func_types[0], + r_values: r_values_p, + func_g_type: func_types[1], + g_values: g_values_p, + func_b_type: func_types[2], + b_values: b_values_p, + func_a_type: func_types[3], + a_values: a_values_p, + }; + return Some(filter_data) + } + } + } + } + } + } + None + } + + fn as_filter_input(&self) -> Option<FilterPrimitiveInput> { + if let Some(input) = self.as_str() { + match input { + "original" => Some(FilterPrimitiveInput::Original), + "previous" => Some(FilterPrimitiveInput::Previous), + _ => None, + } + } else if let Some(index) = self.as_i64() { + if index >= 0 { + Some(FilterPrimitiveInput::OutputOfPrimitiveIndex(index as usize)) + } else { + panic!("Filter input index cannot be negative"); + } + } else { + panic!("Invalid filter input"); + } + } + + fn as_vec_filter_data(&self) -> Option<Vec<FilterData>> { + if let Some(v) = self.as_vec() { + Some(v.iter().map(|x| x.as_filter_data().unwrap()).collect()) + } else { + self.as_filter_data().map(|data| vec![data]) + } + } + + fn as_filter_primitive(&self) -> Option<FilterPrimitive> { + if let Some(filter_type) = self["type"].as_str() { + let kind = match filter_type { + "identity" => { + FilterPrimitiveKind::Identity(IdentityPrimitive { + input: self["in"].as_filter_input().unwrap(), + }) + } + "blend" => { + FilterPrimitiveKind::Blend(BlendPrimitive { + input1: self["in1"].as_filter_input().unwrap(), + input2: self["in2"].as_filter_input().unwrap(), + mode: self["blend-mode"].as_mix_blend_mode().unwrap(), + }) + } + "flood" => { + FilterPrimitiveKind::Flood(FloodPrimitive { + color: self["color"].as_colorf().unwrap(), + }) + } + "blur" => { + FilterPrimitiveKind::Blur(BlurPrimitive { + input: self["in"].as_filter_input().unwrap(), + width: self["width"].as_f32().unwrap(), + height: self["height"].as_f32().unwrap(), + }) + } + "opacity" => { + FilterPrimitiveKind::Opacity(OpacityPrimitive { + input: self["in"].as_filter_input().unwrap(), + opacity: self["opacity"].as_f32().unwrap(), + }) + } + "color-matrix" => { + let m: Vec<f32> = self["matrix"].as_vec_f32().unwrap(); + let mut matrix: [f32; 20] = [0.0; 20]; + matrix.clone_from_slice(&m); + + FilterPrimitiveKind::ColorMatrix(ColorMatrixPrimitive { + input: self["in"].as_filter_input().unwrap(), + matrix, + }) + } + "drop-shadow" => { + FilterPrimitiveKind::DropShadow(DropShadowPrimitive { + input: self["in"].as_filter_input().unwrap(), + shadow: Shadow { + offset: self["offset"].as_vector().unwrap(), + color: self["color"].as_colorf().unwrap(), + blur_radius: self["radius"].as_f32().unwrap(), + } + }) + } + "component-transfer" => { + FilterPrimitiveKind::ComponentTransfer(ComponentTransferPrimitive { + input: self["in"].as_filter_input().unwrap(), + }) + } + "offset" => { + FilterPrimitiveKind::Offset(OffsetPrimitive { + input: self["in"].as_filter_input().unwrap(), + offset: self["offset"].as_vector().unwrap(), + }) + } + "composite" => { + let operator = match self["operator"].as_str().unwrap() { + "over" => CompositeOperator::Over, + "in" => CompositeOperator::In, + "out" => CompositeOperator::Out, + "atop" => CompositeOperator::Atop, + "xor" => CompositeOperator::Xor, + "lighter" => CompositeOperator::Lighter, + "arithmetic" => { + let k_vals = self["k-values"].as_vec_f32().unwrap(); + assert!(k_vals.len() == 4, "Must be 4 k values for arithmetic composite operator"); + let k_vals = [k_vals[0], k_vals[1], k_vals[2], k_vals[3]]; + CompositeOperator::Arithmetic(k_vals) + } + _ => panic!("Invalid composite operator"), + }; + FilterPrimitiveKind::Composite(CompositePrimitive { + input1: self["in1"].as_filter_input().unwrap(), + input2: self["in2"].as_filter_input().unwrap(), + operator, + }) + } + _ => return None, + }; + + Some(FilterPrimitive { + kind, + color_space: self["color-space"].as_color_space().unwrap_or(ColorSpace::LinearRgb), + }) + } else { + None + } + } + + fn as_vec_filter_primitive(&self) -> Option<Vec<FilterPrimitive>> { + if let Some(v) = self.as_vec() { + Some(v.iter().map(|x| x.as_filter_primitive().unwrap()).collect()) + } else { + self.as_filter_primitive().map(|data| vec![data]) + } + } + + fn as_color_space(&self) -> Option<ColorSpace> { + self.as_str().and_then(StringEnum::from_str) + } + + fn as_complex_clip_region(&self) -> ComplexClipRegion { + let rect = self["rect"] + .as_rect() + .expect("Complex clip entry must have rect"); + let radius = self["radius"] + .as_border_radius() + .unwrap_or_else(BorderRadius::zero); + let mode = self["clip-mode"] + .as_clip_mode() + .unwrap_or(ClipMode::Clip); + ComplexClipRegion::new(rect, radius, mode) + } + + fn as_sticky_offset_bounds(&self) -> StickyOffsetBounds { + match *self { + Yaml::Array(ref array) => StickyOffsetBounds::new( + array[0].as_f32().unwrap_or(0.0), + array[1].as_f32().unwrap_or(0.0), + ), + _ => StickyOffsetBounds::new(0.0, 0.0), + } + } + + fn as_gradient(&self, dl: &mut DisplayListBuilder) -> Gradient { + let start = self["start"].as_point().expect("gradient must have start"); + let end = self["end"].as_point().expect("gradient must have end"); + let stops = self["stops"] + .as_vec() + .expect("gradient must have stops") + .chunks(2) + .map(|chunk| { + GradientStop { + offset: chunk[0] + .as_force_f32() + .expect("gradient stop offset is not f32"), + color: chunk[1] + .as_colorf() + .expect("gradient stop color is not color"), + } + }) + .collect::<Vec<_>>(); + let extend_mode = if self["repeat"].as_bool().unwrap_or(false) { + ExtendMode::Repeat + } else { + ExtendMode::Clamp + }; + + dl.create_gradient(start, end, stops, extend_mode) + } + + fn as_radial_gradient(&self, dl: &mut DisplayListBuilder) -> RadialGradient { + let center = self["center"].as_point().expect("radial gradient must have center"); + let radius = self["radius"].as_size().expect("radial gradient must have a radius"); + let stops = self["stops"] + .as_vec() + .expect("radial gradient must have stops") + .chunks(2) + .map(|chunk| { + GradientStop { + offset: chunk[0] + .as_force_f32() + .expect("gradient stop offset is not f32"), + color: chunk[1] + .as_colorf() + .expect("gradient stop color is not color"), + } + }) + .collect::<Vec<_>>(); + let extend_mode = if self["repeat"].as_bool().unwrap_or(false) { + ExtendMode::Repeat + } else { + ExtendMode::Clamp + }; + + dl.create_radial_gradient(center, radius, stops, extend_mode) + } + + fn as_conic_gradient(&self, dl: &mut DisplayListBuilder) -> ConicGradient { + let center = self["center"].as_point().expect("conic gradient must have center"); + let angle = self["angle"].as_force_f32().expect("conic gradient must have an angle"); + let stops = self["stops"] + .as_vec() + .expect("conic gradient must have stops") + .chunks(2) + .map(|chunk| { + GradientStop { + offset: chunk[0] + .as_force_f32() + .expect("gradient stop offset is not f32"), + color: chunk[1] + .as_colorf() + .expect("gradient stop color is not color"), + } + }) + .collect::<Vec<_>>(); + let extend_mode = if self["repeat"].as_bool().unwrap_or(false) { + ExtendMode::Repeat + } else { + ExtendMode::Clamp + }; + + dl.create_conic_gradient(center, angle, stops, extend_mode) + } + + fn as_complex_clip_regions(&self) -> Vec<ComplexClipRegion> { + match *self { + Yaml::Array(ref array) => array + .iter() + .map(Yaml::as_complex_clip_region) + .collect(), + Yaml::BadValue => vec![], + _ => { + println!("Unable to parse complex clip region {:?}", self); + vec![] + } + } + } + + fn as_rotation(&self) -> Option<Rotation> { + match *self { + Yaml::Integer(0) => Some(Rotation::Degree0), + Yaml::Integer(90) => Some(Rotation::Degree90), + Yaml::Integer(180) => Some(Rotation::Degree180), + Yaml::Integer(270) => Some(Rotation::Degree270), + Yaml::BadValue => None, + _ => { + println!("Unable to parse rotation {:?}", self); + None + } + } + } +} |