/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
usecrate::{WindowWrapper, NotifierEvent}; use base64::Engine as _; use image::load as load_piston_image; use image::png::PNGEncoder; use image::{ColorType, ImageFormat}; usecrate::parse_function::parse_function; usecrate::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::*; usecrate::wrench::{Wrench, WrenchThing}; usecrate::yaml_frame_reader::YamlFrameReader;
#[derive(Debug, Copy, Clone)] pubenum 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 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.
// 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' letmut previous_max_diff = 0;
// loop invariant: this is the number of pixels to ignore as they have been counted // against previous iterations' fuzzy statements. letmut previous_sum_fail = 0; // == prefix_sum[previous_max_diff]
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;
}
for (a, b) inself.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);
}
}
pubfn create_data_uri(mutself) -> 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 in0 .. 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];
(&mutself.data[dst_start .. dst_start + stride])
.clone_from_slice(&src_slice[.. stride]);
}
letmut 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::engine::general_purpose::STANDARD.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);
letmut reftests = Vec::new();
for line in file.lines() { let l = line.unwrap();
let expect_usize = &|opt: Option<usize>, msg| { match opt {
Some(val) => val,
None => {
panic!("Parsing error in {}. {:?}: {:?}", msg, manifest, l)
}
}
}; let expect_bool = &|opt: Option<bool>, msg| { match opt {
Some(val) => val,
None => {
panic!("Parsing error in {}. {:?}: {:?}", msg, manifest, l)
}
}
};
// 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();
letmut 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 !expect_bool(environment.parse_condition(args.remove(0)), "unknown condition") { returntrue;
}
fuzziness.clear();
} let num_range = args.len() / 2; for range in0..num_range { letmut max = args[range * 2 ]; letmut 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 !expect_bool(environment.parse_condition(args.remove(0)), "unknown condition") { returntrue;
}
fuzziness.clear();
} let max_difference = expect_usize(args[0].parse().ok(), "max difference"); let num_differences = expect_usize(args[1].parse().ok(), "num differing pixels");
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;
}
}
_ => returnfalse,
} true
};
letmut 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]);
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 expect_bool(environment.parse_condition(args[0]), "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 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);
}
}
}
}
fn version(_wrench: &Wrench, window: &WindowWrapper) -> Option<semver::Version> { if window.is_software() {
None
} elseif 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; letmut 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 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])?)
}
op if op.starts_with("or(") => { let (_, args, _) = parse_function(op); if args.is_empty() { return None;
} letmut any = false; for arg in args.iter() {
any = any | self.parse_condition(arg)?
}
Some(any)
}
op if op.starts_with("and(") => { let (_, args, _) = parse_function(op); if args.is_empty() { return None;
} letmut all = true; for arg in args.iter() {
all = all & self.parse_condition(arg)?
}
Some(all)
}
_ => None,
}
}
}
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. letmut images = vec![]; letmut 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))
);
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); returnfalse;
}
}
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,
) { returnfalse;
}
}
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(&mutself, 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.0as i32, size.1as i32),
}
}
// 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);
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.