summaryrefslogtreecommitdiffstats
path: root/gfx/wr/wrench/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /gfx/wr/wrench/src
parentInitial commit. (diff)
downloadfirefox-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.rs62
-rw-r--r--gfx/wr/wrench/src/args.yaml188
-rw-r--r--gfx/wr/wrench/src/blob.rs213
-rw-r--r--gfx/wr/wrench/src/egl.rs611
-rw-r--r--gfx/wr/wrench/src/main.rs1040
-rw-r--r--gfx/wr/wrench/src/parse_function.rs134
-rw-r--r--gfx/wr/wrench/src/perf.rs349
-rw-r--r--gfx/wr/wrench/src/png.rs118
-rw-r--r--gfx/wr/wrench/src/premultiply.rs56
-rw-r--r--gfx/wr/wrench/src/rawtest.rs1450
-rw-r--r--gfx/wr/wrench/src/reftest.rs970
-rw-r--r--gfx/wr/wrench/src/test_invalidation.rs129
-rw-r--r--gfx/wr/wrench/src/test_shaders.rs161
-rw-r--r--gfx/wr/wrench/src/wrench.rs641
-rw-r--r--gfx/wr/wrench/src/yaml_frame_reader.rs2125
-rw-r--r--gfx/wr/wrench/src/yaml_helper.rs923
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
+ }
+ }
+ }
+}