Bug 1609805 - Support a new reftest kind, for verifying rasterizer accuracy. r=nical,Bert

This patch introduces a new reftest (syntax ** or !* in reftest files).

This type of test renders a single input file multiple times, at a range
of different picture cache tile sizes. It then verifies that each of the
images matches (or doesn't).

This can be used to verify rasterizer accuracy when drawing primitives
with different tile sizes and/or dirty rect update strategies.

One of the included tests in this patch fails the accuracy test - the
intent is to fix this inaccuracy in a follow up patch and then be able to
mark it pixel exact.

Differential Revision: https://phabricator.services.mozilla.com/D60185

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Glenn Watson 2020-01-19 19:45:37 +00:00
Родитель 6802b12993
Коммит 3bf8ad7884
14 изменённых файлов: 318 добавлений и 79 удалений

Просмотреть файл

@ -65,6 +65,7 @@ pub struct FrameBuilderConfig {
pub batch_lookback_count: usize,
pub background_color: Option<ColorF>,
pub compositor_kind: CompositorKind,
pub tile_size_override: Option<DeviceIntSize>,
}
/// A set of common / global resources that are retained between
@ -117,7 +118,7 @@ pub struct FrameVisibilityContext<'a> {
pub surfaces: &'a [SurfaceInfo],
pub debug_flags: DebugFlags,
pub scene_properties: &'a SceneProperties,
pub config: &'a FrameBuilderConfig,
pub config: FrameBuilderConfig,
}
pub struct FrameVisibilityState<'a> {
@ -242,6 +243,7 @@ impl FrameBuilder {
texture_cache_profile: &mut TextureCacheProfileCounters,
composite_state: &mut CompositeState,
tile_cache_logger: &mut TileCacheLogger,
config: FrameBuilderConfig,
) -> Option<RenderTaskId> {
profile_scope!("cull");
@ -327,7 +329,7 @@ impl FrameBuilder {
surfaces,
debug_flags,
scene_properties,
config: &scene.config,
config,
};
let mut visibility_state = FrameVisibilityState {
@ -472,6 +474,7 @@ impl FrameBuilder {
render_task_counters: &mut RenderTaskGraphCounters,
debug_flags: DebugFlags,
tile_cache_logger: &mut TileCacheLogger,
config: FrameBuilderConfig,
) -> Frame {
profile_scope!("build");
profile_marker!("BuildFrame");
@ -542,6 +545,7 @@ impl FrameBuilder {
&mut resource_profile.texture_cache,
&mut composite_state,
tile_cache_logger,
config,
);
let mut passes;

Просмотреть файл

@ -278,13 +278,38 @@ pub const TILE_SIZE_SCROLLBAR_VERTICAL: DeviceIntSize = DeviceIntSize {
_unit: marker::PhantomData,
};
const TILE_SIZE_FOR_TESTS: [DeviceIntSize; 6] = [
DeviceIntSize {
width: 128,
height: 128,
_unit: marker::PhantomData,
},
DeviceIntSize {
width: 256,
height: 256,
_unit: marker::PhantomData,
},
DeviceIntSize {
width: 512,
height: 512,
_unit: marker::PhantomData,
},
TILE_SIZE_DEFAULT,
TILE_SIZE_SCROLLBAR_VERTICAL,
TILE_SIZE_SCROLLBAR_HORIZONTAL,
];
// Return the list of tile sizes for the renderer to allocate texture arrays for.
pub fn tile_cache_sizes() -> &'static [DeviceIntSize] {
&[
TILE_SIZE_DEFAULT,
TILE_SIZE_SCROLLBAR_HORIZONTAL,
TILE_SIZE_SCROLLBAR_VERTICAL,
]
pub fn tile_cache_sizes(testing: bool) -> &'static [DeviceIntSize] {
if testing {
&TILE_SIZE_FOR_TESTS
} else {
&[
TILE_SIZE_DEFAULT,
TILE_SIZE_SCROLLBAR_HORIZONTAL,
TILE_SIZE_SCROLLBAR_VERTICAL,
]
}
}
/// The maximum size per axis of a surface,
@ -1654,6 +1679,9 @@ pub struct TileCacheInstance {
pub device_position: DevicePoint,
/// True if the entire picture cache surface is opaque.
is_opaque: bool,
/// The currently considered tile size override. Used to check if we should
/// re-evaluate tile size, even if the frame timer hasn't expired.
tile_size_override: Option<DeviceIntSize>,
}
impl TileCacheInstance {
@ -1704,6 +1732,7 @@ impl TileCacheInstance {
native_surface_id: None,
device_position: DevicePoint::zero(),
is_opaque: true,
tile_size_override: None,
}
}
@ -1861,22 +1890,28 @@ impl TileCacheInstance {
// Only evaluate what tile size to use fairly infrequently, so that we don't end
// up constantly invalidating and reallocating tiles if the picture rect size is
// changing near a threshold value.
if self.frames_until_size_eval == 0 {
if self.frames_until_size_eval == 0 ||
self.tile_size_override != frame_context.config.tile_size_override {
const TILE_SIZE_TINY: f32 = 32.0;
// Work out what size tile is appropriate for this picture cache.
let desired_tile_size;
// There's no need to check the other dimension. If we encounter a picture
// that is small on one dimension, it's a reasonable choice to use a scrollbar
// sized tile configuration regardless of the other dimension.
if pic_rect.size.width <= TILE_SIZE_TINY {
desired_tile_size = TILE_SIZE_SCROLLBAR_VERTICAL;
} else if pic_rect.size.height <= TILE_SIZE_TINY {
desired_tile_size = TILE_SIZE_SCROLLBAR_HORIZONTAL;
} else {
desired_tile_size = TILE_SIZE_DEFAULT;
}
let desired_tile_size = match frame_context.config.tile_size_override {
Some(tile_size_override) => {
tile_size_override
}
None => {
// There's no need to check the other dimension. If we encounter a picture
// that is small on one dimension, it's a reasonable choice to use a scrollbar
// sized tile configuration regardless of the other dimension.
if pic_rect.size.width <= TILE_SIZE_TINY {
TILE_SIZE_SCROLLBAR_VERTICAL
} else if pic_rect.size.height <= TILE_SIZE_TINY {
TILE_SIZE_SCROLLBAR_HORIZONTAL
} else {
TILE_SIZE_DEFAULT
}
}
};
// If the desired tile size has changed, then invalidate and drop any
// existing tiles.
@ -1893,6 +1928,7 @@ impl TileCacheInstance {
// Reset counter until next evaluating the desired tile size. This is an
// arbitrary value.
self.frames_until_size_eval = 120;
self.tile_size_override = frame_context.config.tile_size_override;
}
// Map an arbitrary point in picture space to world space, to work out

Просмотреть файл

@ -541,6 +541,7 @@ impl Document {
resource_profile: &mut ResourceProfileCounters,
debug_flags: DebugFlags,
tile_cache_logger: &mut TileCacheLogger,
config: FrameBuilderConfig,
) -> RenderedDocument {
let accumulated_scale_factor = self.view.accumulated_scale_factor();
let pan = self.view.pan.to_f32() / accumulated_scale_factor;
@ -568,6 +569,7 @@ impl Document {
&mut self.render_task_counters,
debug_flags,
tile_cache_logger,
config,
);
self.hit_tester = Some(self.scene.create_hit_tester(&self.data_stores.clip));
frame
@ -1142,6 +1144,11 @@ impl RenderBackend {
// We don't want to forward this message to the renderer.
return RenderBackendStatus::Continue;
}
DebugCommand::SetPictureTileSize(tile_size) => {
self.frame_config.tile_size_override = tile_size;
return RenderBackendStatus::Continue;
}
DebugCommand::FetchDocuments => {
// Ask SceneBuilderThread to send JSON presentation of the documents,
// that will be forwarded to Renderer.
@ -1534,6 +1541,7 @@ impl RenderBackend {
&mut profile_counters.resources,
self.debug_flags,
&mut self.tile_cache_logger,
self.frame_config,
);
debug!("generated frame for document {:?} with {} passes",
@ -1712,6 +1720,7 @@ impl RenderBackend {
&mut profile_counters.resources,
self.debug_flags,
&mut self.tile_cache_logger,
self.frame_config,
);
// After we rendered the frames, there are pending updates to both
// GPU cache and resources. Instead of serializing them, we are going to make sure

Просмотреть файл

@ -2185,6 +2185,7 @@ impl Renderer {
batch_lookback_count: options.batch_lookback_count,
background_color: options.clear_color,
compositor_kind,
tile_size_override: None,
};
info!("WR {:?}", config);
@ -2292,7 +2293,7 @@ impl Renderer {
max_texture_size,
max_texture_layers,
if config.global_enable_picture_caching {
tile_cache_sizes()
tile_cache_sizes(config.testing)
} else {
&[]
},
@ -2845,7 +2846,8 @@ impl Renderer {
fn handle_debug_command(&mut self, command: DebugCommand) {
match command {
DebugCommand::EnableDualSourceBlending(_) |
DebugCommand::SetTransactionLogging(_) => {
DebugCommand::SetTransactionLogging(_) |
DebugCommand::SetPictureTileSize(_) => {
panic!("Should be handled by render backend");
}
DebugCommand::FetchDocuments |

Просмотреть файл

@ -258,6 +258,7 @@ impl BuiltScene {
batch_lookback_count: 0,
background_color: None,
compositor_kind: CompositorKind::default(),
tile_size_override: None,
},
}
}

Просмотреть файл

@ -977,6 +977,8 @@ pub enum DebugCommand {
SimulateLongLowPrioritySceneBuild(u32),
/// Logs transactions to a file for debugging purposes
SetTransactionLogging(bool),
/// Set an override tile size to use for picture caches
SetPictureTileSize(Option<DeviceIntSize>),
}
/// Message sent by the `RenderApi` to the render backend thread.

Просмотреть файл

@ -28,5 +28,5 @@ platform(linux,mac) == small-dotted-border.yaml small-dotted-border.png
platform(linux,mac) == border-dashed-dotted-caching.yaml border-dashed-dotted-caching.png
!= small-inset-outset.yaml small-inset-outset-notref.yaml
fuzzy(1,16) == no-aa.yaml green-square.yaml
skip_on(android,device) border-double-1px.yaml border-double-1px-ref.yaml # Fails on Pixel2
skip_on(android,device) == border-double-1px.yaml border-double-1px-ref.yaml # Fails on Pixel2
== max-scale.yaml max-scale-ref.yaml

Просмотреть файл

@ -15,3 +15,4 @@ include snap/reftest.list
include split/reftest.list
include text/reftest.list
include transforms/reftest.list
include tiles/reftest.list

Просмотреть файл

@ -73,5 +73,5 @@ options(disable-aa) == snap-clip.yaml snap-clip-ref.yaml
platform(linux) == perspective-clip.yaml perspective-clip.png
fuzzy(1,39) options(disable-subpixel) == raster-space-snap.yaml raster-space-snap-ref.yaml
# == intermediate-transform.yaml intermediate-transform-ref.yaml # fails because of AA inavailable with an intermediate surface
platform(linux) allow_sacrificing_subpixel_aa(false) text-fixed-slice.yaml text-fixed-slice-slow.png
platform(linux) allow_sacrificing_subpixel_aa(true) text-fixed-slice.yaml text-fixed-slice-fast.png
platform(linux) allow_sacrificing_subpixel_aa(false) == text-fixed-slice.yaml text-fixed-slice-slow.png
platform(linux) allow_sacrificing_subpixel_aa(true) == text-fixed-slice.yaml text-fixed-slice-fast.png

Просмотреть файл

@ -0,0 +1,45 @@
---
root:
items:
- type: stacking-context
bounds: [50, 50, 100, 100]
transform: rotate(30)
items:
- type: rect
bounds: [ 10, 10, 80, 80 ]
color: [0, 255, 0]
- type: box-shadow
bounds: [ 10, 10, 80, 80 ]
blur-radius: 25
clip-mode: inset
- type: rect
bounds: [ 140, 10, 80, 80 ]
color: [0, 255, 0]
- type: box-shadow
bounds: [ 140, 10, 80, 80 ]
blur-radius: 25
clip-mode: outset
- type: border
bounds: [ 250, 10, 100, 100 ]
width: [ 10, 10, 10, 10 ]
border-type: normal
style: solid
color: [ red, green, blue, black ]
radius: {
top-left: [20, 20],
top-right: [10, 10],
bottom-left: [25, 25],
bottom-right: [0, 0],
}
- bounds: [150, 150, 128, 128]
image: checkerboard(4, 15, 8)
stretch-size: 128 128
- type: radial-gradient
bounds: 300 150 100 100
center: 50 50
radius: 50 50
stops: [0, red, 1, blue]

Просмотреть файл

@ -0,0 +1,6 @@
---
root:
items:
- type: rect
bounds: 50 50 200 200
color: red

Просмотреть файл

@ -0,0 +1,4 @@
** rect.yaml
** simple-gradient.yaml
# TODO: Fix rasterizer inaccuracies so this is the same regardless of tile size!
!* prim-suite.yaml

Просмотреть файл

@ -0,0 +1,9 @@
---
root:
items:
- type: gradient
bounds: [ 0, 0, 1980, 1080]
start: [ 0, -2000 ]
end: [ 0, 4000 ]
stops: [ 0.0, red, 1.0, green ]
repeat: false

Просмотреть файл

@ -44,9 +44,16 @@ impl ReftestOptions {
}
}
#[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 {
@ -57,6 +64,8 @@ impl Display for ReftestOp {
match *self {
ReftestOp::Equal => "==".to_owned(),
ReftestOp::NotEqual => "!=".to_owned(),
ReftestOp::Accurate => "**".to_owned(),
ReftestOp::Inaccurate => "!*".to_owned(),
}
)
}
@ -102,6 +111,62 @@ pub struct Reftest {
allow_sacrificing_subpixel_aa: Option<bool>,
}
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 { max_difference, count_different } => {
if max_difference > self.max_difference || count_different > self.num_differences {
println!(
"{} | {} | {}: {}, {}: {}",
"REFTEST TEST-UNEXPECTED-FAIL",
self,
"image comparison, max difference",
max_difference,
"number of differing pixels",
count_different
);
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
}
}
}
}
/// Check the negative case (expecting inequality) and report details if same
fn check_and_report_inequality_failure(
&self,
comparison: ReftestImageComparison,
) -> bool {
match comparison {
ReftestImageComparison::Equal => {
println!("REFTEST TEST-UNEXPECTED-FAIL | {} | image comparison", self);
println!("REFTEST TEST-END | {}", self);
false
}
ReftestImageComparison::NotEqual { .. } => {
true
}
}
}
}
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();
@ -115,10 +180,13 @@ impl Display for Reftest {
}
}
#[derive(Clone)]
pub struct ReftestImage {
pub data: Vec<u8>,
pub size: DeviceIntSize,
}
#[derive(Debug, Copy, Clone)]
pub enum ReftestImageComparison {
Equal,
NotEqual {
@ -212,7 +280,7 @@ impl ReftestManifest {
let mut max_difference = 0;
let mut max_count = 0;
let mut op = ReftestOp::Equal;
let mut op = None;
let mut font_render_mode = None;
let mut extra_checks = vec![];
let mut disable_dual_source_blending = false;
@ -299,10 +367,16 @@ impl ReftestManifest {
}
}
"==" => {
op = ReftestOp::Equal;
op = Some(ReftestOp::Equal);
}
"!=" => {
op = ReftestOp::NotEqual;
op = Some(ReftestOp::NotEqual);
}
"**" => {
op = Some(ReftestOp::Accurate);
}
"!*" => {
op = Some(ReftestOp::Inaccurate);
}
_ => {
paths.push(dir.join(*token));
@ -311,10 +385,13 @@ impl ReftestManifest {
}
// Don't try to add tests for include lines.
if paths.len() < 2 {
assert_eq!(paths.len(), 0, "Only one path provided: {:?}", paths[0]);
continue;
}
let op = match op {
Some(op) => op,
None => {
assert!(paths.is_empty(), format!("paths = {:?}", paths));
continue;
}
};
// The reference is the last path provided. If multiple paths are
// passed for the test, they render sequentially before being
@ -530,15 +607,52 @@ impl<'a> ReftestHarness<'a> {
let mut images = vec![];
let mut results = vec![];
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);
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 = match reference_image {
@ -575,44 +689,50 @@ impl<'a> ReftestHarness<'a> {
}
}
let test = images.pop().unwrap();
let comparison = test.compare(&reference);
match (&t.op, comparison) {
(&ReftestOp::Equal, ReftestImageComparison::Equal) => true,
(
&ReftestOp::Equal,
ReftestImageComparison::NotEqual {
max_difference,
count_different,
},
) => if max_difference > t.max_difference || count_different > t.num_differences {
println!(
"{} | {} | {}: {}, {}: {}",
"REFTEST TEST-UNEXPECTED-FAIL",
t,
"image comparison, max difference",
max_difference,
"number of differing pixels",
count_different
);
println!("REFTEST IMAGE 1 (TEST): {}", test.create_data_uri());
println!(
"REFTEST IMAGE 2 (REFERENCE): {}",
reference.create_data_uri()
);
println!("REFTEST TEST-END | {}", t);
false
} else {
true
},
(&ReftestOp::NotEqual, ReftestImageComparison::Equal) => {
println!("REFTEST TEST-UNEXPECTED-FAIL | {} | image comparison", t);
println!("REFTEST TEST-END | {}", t);
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);
t.check_and_report_inequality_failure(comparison)
}
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 mut found_mismatch = false;
for test in images.drain(..) {
let comparison = test.compare(&reference);
found_mismatch |= t.check_and_report_inequality_failure(comparison);
}
found_mismatch
}
(&ReftestOp::NotEqual, ReftestImageComparison::NotEqual { .. }) => true,
}
}