snapping.rs (17012B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 use crate::RawtestHarness; 6 use crate::euclid::point2; 7 use webrender::api::*; 8 use webrender::api::units::*; 9 use webrender::Transaction; 10 11 struct SnapTestContext { 12 root_spatial_id: SpatialId, 13 test_size: FramebufferIntSize, 14 font_size: f32, 15 ahem_font_key: FontInstanceKey, 16 variant: usize, 17 } 18 19 enum SnapTestExpectation { 20 Rect { 21 expected_color: ColorU, 22 expected_rect: DeviceIntRect, 23 expected_offset: i32, 24 } 25 } 26 27 struct ScrollRequest { 28 external_scroll_id: ExternalScrollId, 29 amount: f32, 30 } 31 32 struct SnapTestResult { 33 scrolls: Vec<ScrollRequest>, 34 expected: SnapTestExpectation, 35 } 36 37 type SnapTestFunction = fn(&mut DisplayListBuilder, &mut SnapTestContext) -> SnapTestResult; 38 39 struct SnapTest { 40 name: &'static str, 41 f: SnapTestFunction, 42 variations: usize, 43 } 44 45 struct SnapVariation { 46 offset: f32, 47 expected: i32, 48 } 49 50 const MAGENTA_RECT: SnapTest = SnapTest { 51 name: "clear", 52 f: dl_clear, 53 variations: 1, 54 }; 55 56 // Types of snap tests 57 const TESTS: &[SnapTest; 4] = &[ 58 // Rectangle, no transform/scroll 59 SnapTest { 60 name: "rect", 61 f: dl_simple_rect, 62 variations: SIMPLE_FRACTIONAL_VARIANTS.len(), 63 }, 64 65 // Glyph, no transform/scroll 66 SnapTest { 67 name: "glyph", 68 f: dl_simple_glyph, 69 variations: SIMPLE_FRACTIONAL_VARIANTS.len(), 70 }, 71 72 // Rect, APZ 73 SnapTest { 74 name: "scroll1", 75 f: dl_scrolling1, 76 variations: SCROLL_VARIANTS.len(), 77 }, 78 79 // Rect, APZ + external scroll offset 80 SnapTest { 81 name: "rect-apz-ext-1", 82 f: dl_scrolling_ext1, 83 variations: EXTERNAL_SCROLL_VARIANTS.len(), 84 } 85 ]; 86 87 // Variants we will run for each snap test with expected float offset and raster difference 88 const SIMPLE_FRACTIONAL_VARIANTS: [SnapVariation; 13] = [ 89 SnapVariation { 90 offset: 0.0, 91 expected: 0, 92 }, 93 SnapVariation { 94 offset: 0.1, 95 expected: 0, 96 }, 97 SnapVariation { 98 offset: 0.25, 99 expected: 0, 100 }, 101 SnapVariation { 102 offset: 0.33, 103 expected: 0, 104 }, 105 SnapVariation { 106 offset: 0.49, 107 expected: 0, 108 }, 109 SnapVariation { 110 offset: 0.5, 111 expected: 1, 112 }, 113 SnapVariation { 114 offset: 0.51, 115 expected: 1, 116 }, 117 SnapVariation { 118 offset: -0.1, 119 expected: 0, 120 }, 121 SnapVariation { 122 offset: -0.25, 123 expected: 0, 124 }, 125 SnapVariation { 126 offset: -0.33, 127 expected: 0, 128 }, 129 SnapVariation { 130 offset: -0.49, 131 expected: 0, 132 }, 133 SnapVariation { 134 offset: -0.5, 135 expected: 0, 136 }, 137 SnapVariation { 138 offset: -0.51, 139 expected: -1, 140 }, 141 ]; 142 143 struct ScrollVariation{ 144 apz_scroll: f32, 145 prim_offset: f32, 146 expected: i32, 147 } 148 149 const SCROLL_VARIANTS: [ScrollVariation; 3] = [ 150 ScrollVariation { 151 apz_scroll: 0.0, 152 prim_offset: 0.0, 153 expected: 0, 154 }, 155 ScrollVariation { 156 apz_scroll: -1.0, 157 prim_offset: 0.0, 158 expected: 1, 159 }, 160 ScrollVariation { 161 apz_scroll: -1.5, 162 prim_offset: 0.0, 163 expected: 2, 164 }, 165 ]; 166 167 struct ExternalScrollVariation{ 168 external_offset: f32, 169 apz_scroll: f32, 170 prim_offset: f32, 171 expected: i32, 172 } 173 174 const EXTERNAL_SCROLL_VARIANTS: [ExternalScrollVariation; 1] = [ 175 ExternalScrollVariation { 176 external_offset: 100.0, 177 apz_scroll: -101.0, 178 prim_offset: -100.0, 179 expected: 1, 180 }, 181 ]; 182 183 impl<'a> RawtestHarness<'a> { 184 pub fn test_snapping(&mut self) { 185 println!("\tsnapping test..."); 186 187 // Test size needs to be: 188 // (a) as small as possible for performance 189 // (b) a factor of 5 (ahem font baseline requirement) 190 // (c) an even number (center placement of test render) 191 let test_size = FramebufferIntSize::new(20, 20); 192 let mut any_fails = false; 193 194 // Load the ahem.css test font 195 let font_bytes = include_bytes!("../../reftests/text/Ahem.ttf").into(); 196 let font_key = self.wrench.font_key_from_bytes(font_bytes, 0); 197 let font_size = 0.5 * test_size.width as f32; 198 let ahem_font_key = self.wrench.add_font_instance( 199 font_key, 200 font_size, 201 FontInstanceFlags::empty(), 202 Some(FontRenderMode::Alpha), 203 SyntheticItalics::disabled(), 204 ); 205 206 // Run each test 207 for test in TESTS { 208 for i in 0 .. test.variations { 209 let mut ctx = SnapTestContext { 210 ahem_font_key, 211 font_size, 212 root_spatial_id: SpatialId::root_scroll_node(self.wrench.root_pipeline_id), 213 test_size, 214 variant: i, 215 }; 216 217 any_fails = !self.run_snap_test(test, &mut ctx); 218 219 // Each test clears to a magenta rect before running the next test. This 220 // ensures that if WR's invalidation logic would skip rendering a test due 221 // to detection that it's the same output, we will still render it to test 222 // the pixel snapping is actually correct 223 assert!(self.run_snap_test(&MAGENTA_RECT, &mut ctx)); 224 } 225 } 226 227 assert!(!any_fails); 228 } 229 230 fn run_snap_test( 231 &mut self, 232 test: &SnapTest, 233 ctx: &mut SnapTestContext, 234 ) -> bool { 235 let mut builder = DisplayListBuilder::new(self.wrench.root_pipeline_id); 236 builder.begin(); 237 238 let result = (test.f)(&mut builder, ctx); 239 240 let window_size = self.window.get_inner_size(); 241 let window_rect = FramebufferIntRect::from_origin_and_size( 242 point2(0, window_size.height - ctx.test_size.height), 243 ctx.test_size, 244 ); 245 246 let txn = Transaction::new(); 247 self.submit_dl(&mut Epoch(0), builder, txn); 248 249 for scroll in result.scrolls { 250 let mut txn = Transaction::new(); 251 txn.set_scroll_offsets( 252 scroll.external_scroll_id, 253 vec![SampledScrollOffset { 254 offset: LayoutVector2D::new(0.0, scroll.amount), 255 generation: APZScrollGeneration::default(), 256 }], 257 ); 258 txn.generate_frame(0, true, false, RenderReasons::TESTING); 259 self.wrench.api.send_transaction(self.wrench.document_id, txn); 260 261 self.render_and_get_pixels(window_rect); 262 } 263 264 let pixels = self.render_and_get_pixels(window_rect); 265 266 let ok = validate_output( 267 &pixels, 268 result.expected, 269 ctx.test_size, 270 ); 271 272 if !ok { 273 println!("FAIL {} [{}]", test.name, ctx.variant); 274 275 // enable to save output as png for debugging 276 // use crate::png; 277 // png::save( 278 // format!("snap_test_{}.png", test.name), 279 // pixels.clone(), 280 // ctx.test_size.cast_unit(), 281 // png::SaveSettings { 282 // flip_vertical: true, 283 // try_crop: false, 284 // }, 285 // ); 286 287 // enable to log output to console for debugging 288 // for y in 0 .. ctx.test_size.height { 289 // for x in 0 .. ctx.test_size.width { 290 // let i = ((ctx.test_size.height - y - 1) * ctx.test_size.width + x) as usize * 4; 291 // let r = pixels[i+0]; 292 // let g = pixels[i+1]; 293 // let b = pixels[i+2]; 294 // let a = pixels[i+3]; 295 // print!("[{:2x},{:2x},{:2x},{:2x}], ", r, g, b, a); 296 // } 297 // print!("\n"); 298 // } 299 } 300 301 ok 302 } 303 } 304 305 fn validate_output( 306 pixels: &[u8], 307 expected: SnapTestExpectation, 308 frame_buffer_size: FramebufferIntSize, 309 ) -> bool { 310 match expected { 311 SnapTestExpectation::Rect { expected_color, expected_rect, expected_offset } => { 312 let expected_rect = expected_rect.translate( 313 DeviceIntVector2D::new(0, expected_offset) 314 ); 315 316 for y in 0 .. frame_buffer_size.height { 317 for x in 0 .. frame_buffer_size.width { 318 let i = ((frame_buffer_size.height - y - 1) * frame_buffer_size.width + x) as usize * 4; 319 let actual = ColorU::new( 320 pixels[i+0], 321 pixels[i+1], 322 pixels[i+2], 323 pixels[i+3], 324 ); 325 326 let expected = if expected_rect.contains(DeviceIntPoint::new(x, y)) { 327 expected_color 328 } else { 329 ColorU::new(255, 255, 255, 255) 330 }; 331 332 if expected != actual { 333 println!("FAILED at ({}, {}):", x, y); 334 println!("\tExpected [{:2x},{:2x},{:2x},{:2x}]", 335 expected.r, 336 expected.g, 337 expected.b, 338 expected.a, 339 ); 340 println!("\tGot [{:2x},{:2x},{:2x},{:2x}]", 341 actual.r, 342 actual.g, 343 actual.b, 344 actual.a, 345 ); 346 return false; 347 } 348 } 349 } 350 351 true 352 } 353 } 354 } 355 356 fn dl_clear( 357 builder: &mut DisplayListBuilder, 358 ctx: &mut SnapTestContext, 359 ) -> SnapTestResult { 360 let color = ColorF::new(1.0, 0.0, 1.0, 1.0); 361 362 let bounds = ctx.test_size 363 .to_f32() 364 .cast_unit() 365 .into(); 366 367 builder.push_rect( 368 &CommonItemProperties { 369 clip_rect: bounds, 370 clip_chain_id: ClipChainId::INVALID, 371 spatial_id: ctx.root_spatial_id, 372 flags: PrimitiveFlags::default(), 373 }, 374 bounds, 375 color, 376 ); 377 378 SnapTestResult { 379 scrolls: vec![], 380 expected: SnapTestExpectation::Rect { 381 expected_color: color.into(), 382 expected_rect: ctx.test_size.cast_unit().into(), 383 expected_offset: 0, 384 } 385 } 386 } 387 388 // Draw a centered rect 389 fn dl_simple_rect( 390 builder: &mut DisplayListBuilder, 391 ctx: &mut SnapTestContext 392 ) -> SnapTestResult { 393 let color = ColorF::BLACK; 394 let variant = &SIMPLE_FRACTIONAL_VARIANTS[ctx.variant]; 395 396 let prim_size = DeviceIntSize::new( 397 ctx.test_size.width / 2, 398 ctx.test_size.height / 2 399 ); 400 401 let rect = DeviceIntRect::from_origin_and_size( 402 DeviceIntPoint::new( 403 (ctx.test_size.width - prim_size.width) / 2, 404 (ctx.test_size.height - prim_size.height) / 2, 405 ), 406 prim_size, 407 ); 408 409 let bounds = rect 410 .to_f32() 411 .cast_unit() 412 .translate( 413 LayoutVector2D::new(0.0, variant.offset) 414 ); 415 416 builder.push_rect( 417 &CommonItemProperties { 418 clip_rect: bounds, 419 clip_chain_id: ClipChainId::INVALID, 420 spatial_id: ctx.root_spatial_id, 421 flags: PrimitiveFlags::default(), 422 }, 423 bounds, 424 color, 425 ); 426 427 SnapTestResult { 428 scrolls: vec![], 429 expected: SnapTestExpectation::Rect { 430 expected_color: color.into(), 431 expected_rect: rect, 432 expected_offset: variant.expected, 433 } 434 } 435 } 436 437 // Draw a centered glyph with ahem.css font 438 fn dl_simple_glyph( 439 builder: &mut DisplayListBuilder, 440 ctx: &mut SnapTestContext, 441 ) -> SnapTestResult { 442 let color = ColorF::BLACK; 443 let variant = &SIMPLE_FRACTIONAL_VARIANTS[ctx.variant]; 444 445 let prim_size = DeviceIntSize::new( 446 ctx.test_size.width / 2, 447 ctx.test_size.height / 2 448 ); 449 450 let rect = DeviceIntRect::from_origin_and_size( 451 DeviceIntPoint::new( 452 (ctx.test_size.width - prim_size.width) / 2, 453 (ctx.test_size.height - prim_size.height) / 2, 454 ), 455 prim_size, 456 ); 457 458 let bounds = rect 459 .to_f32() 460 .cast_unit() 461 .translate( 462 LayoutVector2D::new(0.0, variant.offset) 463 ); 464 465 builder.push_text( 466 &CommonItemProperties { 467 clip_rect: bounds, 468 clip_chain_id: ClipChainId::INVALID, 469 spatial_id: ctx.root_spatial_id, 470 flags: PrimitiveFlags::default(), 471 }, 472 bounds, 473 &[ 474 GlyphInstance { 475 index: 0x41, 476 point: LayoutPoint::new( 477 bounds.min.x, 478 // ahem.css font has baseline at 0.8em 479 bounds.min.y + ctx.font_size * 0.8, 480 ), 481 } 482 ], 483 ctx.ahem_font_key, 484 color, 485 None, 486 ); 487 488 SnapTestResult { 489 scrolls: vec![], 490 expected: SnapTestExpectation::Rect { 491 expected_color: color.into(), 492 expected_rect: rect, 493 expected_offset: variant.expected, 494 } 495 } 496 } 497 498 fn dl_scrolling1( 499 builder: &mut DisplayListBuilder, 500 ctx: &mut SnapTestContext, 501 ) -> SnapTestResult { 502 let color = ColorF::BLACK; 503 let external_scroll_id = ExternalScrollId(1, PipelineId::dummy()); 504 let variant = &SCROLL_VARIANTS[ctx.variant]; 505 506 let scroll_id = builder.define_scroll_frame( 507 ctx.root_spatial_id, 508 external_scroll_id, 509 LayoutRect::from_size(LayoutSize::new(100.0, 1000.0)), 510 LayoutRect::from_size(LayoutSize::new(100.0, 100.0)), 511 LayoutVector2D::zero(), 512 APZScrollGeneration::default(), 513 HasScrollLinkedEffect::No, 514 SpatialTreeItemKey::new(0, 0), 515 ); 516 517 let prim_size = DeviceIntSize::new( 518 ctx.test_size.width / 2, 519 ctx.test_size.height / 2 520 ); 521 522 let rect = DeviceIntRect::from_origin_and_size( 523 DeviceIntPoint::new( 524 (ctx.test_size.width - prim_size.width) / 2, 525 (ctx.test_size.height - prim_size.height) / 2, 526 ), 527 prim_size, 528 ); 529 530 let bounds = rect 531 .to_f32() 532 .cast_unit() 533 .translate( 534 LayoutVector2D::new(0.0, variant.prim_offset) 535 ); 536 537 builder.push_rect( 538 &CommonItemProperties { 539 clip_rect: bounds, 540 clip_chain_id: ClipChainId::INVALID, 541 spatial_id: scroll_id, 542 flags: PrimitiveFlags::default(), 543 }, 544 bounds, 545 color, 546 ); 547 548 SnapTestResult { 549 scrolls: vec![ 550 ScrollRequest { 551 external_scroll_id, 552 amount: variant.apz_scroll, 553 } 554 ], 555 expected: SnapTestExpectation::Rect { 556 expected_color: color.into(), 557 expected_rect: rect, 558 expected_offset: variant.expected, 559 } 560 } 561 } 562 563 fn dl_scrolling_ext1( 564 builder: &mut DisplayListBuilder, 565 ctx: &mut SnapTestContext, 566 ) -> SnapTestResult { 567 let color = ColorF::BLACK; 568 let external_scroll_id = ExternalScrollId(1, PipelineId::dummy()); 569 let variant = &EXTERNAL_SCROLL_VARIANTS[ctx.variant]; 570 571 let scroll_id = builder.define_scroll_frame( 572 ctx.root_spatial_id, 573 external_scroll_id, 574 LayoutRect::from_size(LayoutSize::new(100.0, 1000.0)), 575 LayoutRect::from_size(LayoutSize::new(100.0, 100.0)), 576 LayoutVector2D::new(0.0, variant.external_offset), 577 APZScrollGeneration::default(), 578 HasScrollLinkedEffect::No, 579 SpatialTreeItemKey::new(0, 0), 580 ); 581 582 let prim_size = DeviceIntSize::new( 583 ctx.test_size.width / 2, 584 ctx.test_size.height / 2 585 ); 586 587 let rect = DeviceIntRect::from_origin_and_size( 588 DeviceIntPoint::new( 589 (ctx.test_size.width - prim_size.width) / 2, 590 (ctx.test_size.height - prim_size.height) / 2, 591 ), 592 prim_size, 593 ); 594 595 let bounds = rect 596 .to_f32() 597 .cast_unit() 598 .translate( 599 LayoutVector2D::new(0.0, variant.prim_offset) 600 ); 601 602 builder.push_rect( 603 &CommonItemProperties { 604 clip_rect: bounds, 605 clip_chain_id: ClipChainId::INVALID, 606 spatial_id: scroll_id, 607 flags: PrimitiveFlags::default(), 608 }, 609 bounds, 610 color, 611 ); 612 613 SnapTestResult { 614 scrolls: vec![ 615 ScrollRequest { 616 external_scroll_id, 617 amount: variant.apz_scroll, 618 } 619 ], 620 expected: SnapTestExpectation::Rect { 621 expected_color: color.into(), 622 expected_rect: rect, 623 expected_offset: variant.expected, 624 } 625 } 626 }