diff options
Diffstat (limited to 'gfx/wr/wrench/src/main.rs')
-rw-r--r-- | gfx/wr/wrench/src/main.rs | 1039 |
1 files changed, 1039 insertions, 0 deletions
diff --git a/gfx/wr/wrench/src/main.rs b/gfx/wr/wrench/src/main.rs new file mode 100644 index 0000000000..30c4580b04 --- /dev/null +++ b/gfx/wr/wrench/src/main.rs @@ -0,0 +1,1039 @@ +/* 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) { + // 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().external_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().external_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 <external_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().external_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(); + } + } + } + _ => {} + } + }); +} |